menu collaged full view and font size adjustment (#162)

* menu sticky and add font adjustment

* menu sticky and add font adjustment

* ui menu sticky and fontsize adjustment

---------

Co-authored-by: phearun <phearun@whitesand.online>
This commit is contained in:
Phearun UM
2025-09-15 13:43:22 +07:00
committed by GitHub
parent 6887dc291b
commit bda862e4d3
21 changed files with 1382 additions and 566 deletions
+24 -20
View File
@@ -1,25 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle</title>
<meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle - Dev</title>
<meta name="description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
<meta property="og:title" content="checkcle-an-open-source" />
<meta property="og:description"
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+184
View File
@@ -8,6 +8,7 @@
"name": "vite_react_shadcn_ts",
"version": "0.0.0",
"dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -644,6 +645,21 @@
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
@@ -663,6 +679,26 @@
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.20.2",
"@react-aria/interactions": "^3.25.0",
"@tanstack/react-virtual": "^3.13.9",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
@@ -2280,6 +2316,103 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/@react-aria/focus": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.5",
"@react-aria/utils": "^3.30.1",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.25.5",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.30.1",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.10.8",
"@react-types/shared": "^3.32.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@@ -2729,6 +2862,15 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@swc/types": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
@@ -2793,6 +2935,33 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -6289,6 +6458,12 @@
"node": ">=12.0.0"
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
@@ -6568,6 +6743,15 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+2
View File
@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "2.2.7",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -45,6 +46,7 @@
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"framer-motion": "12.23.12",
"input-otp": "^1.2.4",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
+139 -45
View File
@@ -1,37 +1,40 @@
import React, { useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from '@/components/ui/sonner';
import React, { useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/sonner";
import { ThemeProvider } from './contexts/ThemeContext';
import { LanguageProvider } from './contexts/LanguageContext';
import { SidebarProvider } from './contexts/SidebarContext';
import { ThemeProvider } from "./contexts/ThemeContext";
import { LanguageProvider } from "./contexts/LanguageContext";
import { SidebarProvider } from "./contexts/SidebarContext";
import Index from './pages/Index';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import InstanceMonitoring from './pages/InstanceMonitoring';
import ContainerMonitoring from './pages/ContainerMonitoring';
import ServiceDetail from './pages/ServiceDetail';
import SslDomain from './pages/SslDomain';
import ScheduleIncident from './pages/ScheduleIncident';
import OperationalPage from './pages/OperationalPage';
import RegionalMonitoring from './pages/RegionalMonitoring';
import Settings from './pages/Settings';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';
import PublicStatusPage from './pages/PublicStatusPage';
import { ProtectedRoute } from './components/auth/ProtectedRoute';
import ServerDetail from './pages/ServerDetail';
import Index from "./pages/Index";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import InstanceMonitoring from "./pages/InstanceMonitoring";
import ContainerMonitoring from "./pages/ContainerMonitoring";
import ServiceDetail from "./pages/ServiceDetail";
import SslDomain from "./pages/SslDomain";
import ScheduleIncident from "./pages/ScheduleIncident";
import OperationalPage from "./pages/OperationalPage";
import RegionalMonitoring from "./pages/RegionalMonitoring";
import Settings from "./pages/Settings";
import Profile from "./pages/Profile";
import NotFound from "./pages/NotFound";
import PublicStatusPage from "./pages/PublicStatusPage";
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
import ServerDetail from "./pages/ServerDetail";
function App() {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000
}
}
}));
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
);
return (
<BrowserRouter>
@@ -43,22 +46,113 @@ function App() {
<Routes>
{/* Public routes */}
<Route path="/public/:slug" element={<PublicStatusPage />} />
{/* Protected routes */}
<Route path="/login" element={<Login />} />
<Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} />
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} />
<Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} />
<Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
<Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
<Route path="/service/:id" element={<ProtectedRoute><ServiceDetail /></ProtectedRoute>} />
<Route path="/ssl-domain" element={<ProtectedRoute><SslDomain /></ProtectedRoute>} />
<Route path="/schedule-incident" element={<ProtectedRoute><ScheduleIncident /></ProtectedRoute>} />
<Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} />
<Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route
path="/"
element={
<ProtectedRoute>
<Index />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/instance-monitoring"
element={
<ProtectedRoute>
<InstanceMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/server-detail/:serverId"
element={
<ProtectedRoute>
<ServerDetail />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/container-monitoring/:serverId"
element={
<ProtectedRoute>
<ContainerMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
<Route
path="/regional-monitoring"
element={
<ProtectedRoute>
<RegionalMonitoring />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</QueryClientProvider>
@@ -69,4 +163,4 @@ function App() {
);
}
export default App;
export default App;
@@ -1,4 +1,3 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
@@ -15,17 +14,24 @@ interface DashboardContentProps {
error: Error | null;
}
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
export const DashboardContent = ({
services,
isLoading,
error,
}: DashboardContentProps) => {
const { t } = useLanguage();
const [filter, setFilter] = useState<string>("all");
const [searchTerm, setSearchTerm] = useState<string>("");
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
// Filter services based on search term and type filter
const filteredServices = services.filter(service => {
const matchesSearch = service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase();
const filteredServices = services.filter((service) => {
const matchesSearch =
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.url &&
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesFilter =
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
return matchesSearch && matchesFilter;
});
@@ -33,40 +39,42 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>Error loading service data.</p>
<Button onClick={() => window.location.reload()}>{t('retry')}</Button>
<Button onClick={() => window.location.reload()}>{t("retry")}</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 ">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-4 rounded-xl ">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground">{t('overview')}</h2>
<Button
<h2 className="text-2xl font-bold text-foreground">
{t("overview")}
</h2>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> {t('newService')}
<Plus className="w-4 h-4 mr-2" /> {t("newService")}
</Button>
</div>
<StatusCards services={services} />
<ServiceFilters
<ServiceFilters
filter={filter}
setFilter={setFilter}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
servicesCount={filteredServices.length}
/>
<div className="flex-1 flex flex-col pb-6">
<ServicesTable services={filteredServices} />
</div>
</div>
<AddServiceDialog
<AddServiceDialog
open={isAddDialogOpen}
onOpenChange={setIsAddDialogOpen}
/>
+208 -57
View File
@@ -1,13 +1,39 @@
import { Button } from "@/components/ui/button";
import { AuthUser } from "@/services/authService";
import { useTheme } from "@/contexts/ThemeContext";
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react";
import {
Moon,
PanelLeft,
PanelLeftClose,
Sun,
Globe,
FileText,
Github,
Twitter,
MessageSquare,
Bell,
User,
Settings,
LogOut,
CaseUpper,
BookA,
CaseSensitive,
CaseLower,
ALargeSmall,
} from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings";
import useDynamicFontSize from "@/contexts/FontSizeContext";
interface HeaderProps {
currentUser: AuthUser | null;
@@ -20,7 +46,7 @@ export const Header = ({
currentUser,
onLogout,
sidebarCollapsed,
toggleSidebar
toggleSidebar,
}: HeaderProps) => {
const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage();
@@ -55,148 +81,270 @@ export const Header = ({
}, [currentUser]);
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
let avatarUrl = '';
let avatarUrl = "";
if (currentUser?.avatar) {
// If it's a relative path from the public folder, make sure it's resolved properly
if (currentUser.avatar.startsWith('/upload/profile/')) {
if (currentUser.avatar.startsWith("/upload/profile/")) {
avatarUrl = currentUser.avatar;
} else {
avatarUrl = currentUser.avatar;
}
console.log("Final avatar URL:", avatarUrl);
}
const [fontSize, setFontSize] = useState(() => {
const savedFontSize = localStorage.getItem("fontSize");
return savedFontSize ? parseInt(savedFontSize, 10) : 14;
});
useDynamicFontSize(fontSize);
return (
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden">
{/* Grid Pattern Overlay - Similar to StatusCards */}
<div className="absolute inset-0 z-0">
<div
className="w-full h-full"
style={{
backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px),
linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`,
backgroundSize: '20px 20px'
<div
className="w-full h-full dark:bg-black"
style={{
backgroundImage: `linear-gradient(${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px),
linear-gradient(90deg, ${
theme === "dark" ? "#ffffff10" : "#00000010"
} 1px, transparent 1px)`,
backgroundSize: "20px 20px",
}}
>
<div className="w-full h-full backdrop-blur-[1px]"></div>
</div>
</div>
{/* Header Content */}
<div className="flex items-center gap-4 z-10">
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
<div
className={` p-1 h-8 w-8 -ml-2 rounded bg-gray-600 ${
!sidebarCollapsed ? "hidden" : ""
}`}
>
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className={`h-6 w-6 `}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="mr-2"
>
{sidebarCollapsed ? (
<PanelLeft className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
</Button>
<div className="flex items-center space-x-2">
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1>
<h1 className="font-medium text-xl">
{greeting},{" "}
{currentUser?.name || currentUser?.email?.split("@")[0] || "User"}{" "}
👋
</h1>
</div>
</div>
<div className="flex items-center space-x-4 z-10">
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={toggleTheme}
>
<span className="sr-only">Toggle theme</span>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
{theme === "dark" ? (
<Sun className="w-4 h-4" />
) : (
<Moon className="w-4 h-4" />
)}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span>
<Globe className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("en")}
className={language === "en" ? "bg-accent" : ""}
>
{t("english")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("km")}
className={language === "km" ? "bg-accent" : ""}
>
{t("khmer")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("de")}
className={language === "de" ? "bg-accent" : ""}
>
{t("Deutsch")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("ko")}
className={language === "ko" ? "bg-accent" : ""}
>
{t("korean")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("ja")}
className={language === "ja" ? "bg-accent" : ""}
>
{t("japanese")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
<DropdownMenuItem
onClick={() => setLanguage("zhcn")}
className={language === "zhcn" ? "bg-accent" : ""}
>
{t("simplifiedChinese")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Documentation */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
>
<span className="sr-only">{t("documentation")}</span>
<FileText className="w-4 h-4" />
</Button>
{/* GitHub */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
onClick={() =>
window.open("https://github.com/operacle/checkcle", "_blank")
}
>
<span className="sr-only">GitHub</span>
<Github className="w-4 h-4" />
</Button>
{/* X (Twitter) */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
>
<span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" />
</Button>
{/* Discord */}
<Button
variant="outline"
size="icon"
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
>
<span className="sr-only">Discord</span>
<MessageSquare className="w-4 h-4" />
</Button>
{/* Notifications */}
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("notifications")}</span>
<Bell className="w-4 h-4" />
</Button>
{/* Fond Size */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
>
<span className="sr-only">{t("language")}</span>
<BookA className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev + 1)}
className={`cursor-pointer`}
>
<span className={`text-[15px] font-semibold`}> Aa +</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setFontSize((prev) => prev - 1)}
className={`cursor-pointer`}
>
<span className={`text-[12px] font-semibold`}> Aa -</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* User Profile Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-3 p-2">
<Avatar className="h-10 w-10">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
{avatarUrl ? (
<AvatarImage
src={avatarUrl}
alt={
currentUser?.name ||
currentUser?.email?.split("@")[0] ||
"User"
}
/>
) : (
<AvatarFallback className="bg-primary/20 text-primary">
{currentUser?.name?.[0] || currentUser?.email?.[0] || "U"}
</AvatarFallback>
)}
</Avatar>
<div className="flex flex-col space-y-1">
<span className="font-medium">{currentUser?.name || 'User'}</span>
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
<span className="font-medium">
{currentUser?.name || "User"}
</span>
<span className="text-xs text-muted-foreground truncate">
{currentUser?.email}
</span>
</div>
</div>
<DropdownMenuSeparator />
@@ -209,7 +357,10 @@ export const Header = ({
<span>{t("settings")}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onLogout} className="text-red-500 focus:text-red-500">
<DropdownMenuItem
onClick={onLogout}
className="text-red-500 focus:text-red-500"
>
<LogOut className="mr-2 h-4 w-4" />
<span>{t("logout")}</span>
</DropdownMenuItem>
@@ -1,7 +1,12 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
@@ -13,29 +18,31 @@ interface ServiceFiltersProps {
servicesCount: number;
}
export const ServiceFilters = ({
filter,
setFilter,
searchTerm,
export const ServiceFilters = ({
filter,
setFilter,
searchTerm,
setSearchTerm,
servicesCount
servicesCount,
}: ServiceFiltersProps) => {
const { t } = useLanguage();
return (
<div className="mb-6 flex justify-between items-center">
<div className="mb-6 flex justify-between items-center ">
<div className="flex items-center">
<h3 className="text-xl font-semibold mr-2 text-foreground">{t('currentlyMonitoring')}</h3>
<h3 className="text-xl font-semibold mr-2 text-foreground">
{t("currentlyMonitoring")}
</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
{servicesCount}
</span>
</div>
<div className="flex space-x-4">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-40 bg-card border-border">
<SelectValue placeholder={t('allTypes')} />
<SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800">
<SelectValue placeholder={t("allTypes")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('allTypes')}</SelectItem>
<SelectItem value="all">{t("allTypes")}</SelectItem>
<SelectItem value="HTTP">HTTP</SelectItem>
<SelectItem value="PING">PING</SelectItem>
<SelectItem value="TCP">TCP</SelectItem>
@@ -43,9 +50,9 @@ export const ServiceFilters = ({
</SelectContent>
</Select>
<div className="relative">
<Input
className="w-72 bg-card border-border"
placeholder={t('search')}
<Input
className="w-72 bg-card border-border"
placeholder={t("search")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -1,4 +1,3 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
import { SidebarHeader } from "./sidebar/SidebarHeader";
@@ -11,18 +10,25 @@ interface SidebarProps {
export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme();
return (
<div
<div
className={`
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
border-r flex flex-col h-full
${collapsed ? 'w-16' : 'w-64'}
`}
${
theme === "dark"
? "bg-black border-[#1e1e1e]"
: "bg-sidebars border-sidebar-border"
}
border-r flex flex-col h-full
${collapsed ? "w-16" : "w-64"}
${collapsed ? "hidden" : ""}`}
>
<SidebarHeader collapsed={collapsed} />
<MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} />
<div className="items-center ml-2 p-2 h-30 border-t ">
Version: latest
</div>
</div>
);
};
};
@@ -1,4 +1,3 @@
import React from "react";
import { MenuItem } from "./MenuItem";
import { mainMenuItems } from "./navigationData";
@@ -7,9 +6,15 @@ interface MainNavigationProps {
collapsed: boolean;
}
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
export const MainNavigation: React.FC<MainNavigationProps> = ({
collapsed,
}) => {
return (
<nav className="my-2 mx-1 py-1 px-1">
<nav
className={`my-2 mx-1 py-1 px-1 dark:bg-black ${
collapsed ? "hidden" : ""
}`}
>
{mainMenuItems.map((item) => (
<MenuItem
key={item.id}
@@ -24,4 +29,4 @@ export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) =>
))}
</nav>
);
};
};
@@ -1,4 +1,3 @@
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
@@ -22,7 +21,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
translationKey,
color,
hasNavigation,
collapsed
collapsed,
}) => {
const { theme } = useTheme();
const { t } = useLanguage();
@@ -32,7 +31,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (hasNavigation && path) {
navigate(path, { replace: false });
}
@@ -42,23 +41,29 @@ export const MenuItem: React.FC<MenuItemProps> = ({
const mainIconSize = "h-6 w-6";
return (
<div
<div
className={`
${collapsed ? 'p-3' : 'p-2 pl-3'}
${collapsed ? "p-3" : "p-2 pl-3"}
mb-1 rounded-lg
${isActive ? (theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent') : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`}
${
isActive
? theme === "dark"
? "bg-drak"
: "bg-sidebar-accent "
: `hover:${theme === "dark" ? "bg-gray-800" : "bg-sidebar-accent "}`
}
flex items-center
${collapsed ? 'justify-center' : ''}
${collapsed ? "justify-center" : ""}
cursor-pointer
`}
onClick={handleClick}
>
<Icon className={`${mainIconSize} ${color}`} />
{!collapsed && (
<span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
<span className="ml-2.5 font-medium text-foreground tracking-wide ">
{t(translationKey)}
</span>
)}
</div>
);
};
};
@@ -1,10 +1,13 @@
import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { Settings, ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { settingsMenuItems } from "./navigationData";
@@ -17,19 +20,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
const { t } = useLanguage();
const location = useLocation();
const navigate = useNavigate();
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>(
"general"
);
//const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [settingsPanelOpen, setSettingsPanelOpen] = useState(() => {
const savedValue = localStorage.getItem("settingsPanelOpen");
// Check if the saved value exists and is "true"
return savedValue === "true";
});
// Update active settings item based on URL
useEffect(() => {
if (location.pathname === '/settings') {
if (location.pathname === "/settings") {
const params = new URLSearchParams(location.search);
const panel = params.get('panel');
const panel = params.get("panel");
if (panel) {
setActiveSettingsItem(panel);
}
}
}, [location]);
localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString());
}, [location, settingsPanelOpen]);
const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item);
@@ -43,15 +54,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
};
const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
return `p-2 ${
isActive
? theme === "dark"
? "bg-[#1a1a1a]"
: "bg-sidebar-accent"
: `hover:${theme === "dark" ? "bg-[#1a1a1a]" : "bg-sidebar-accent"}`
} rounded-lg flex items-center `;
};
if (collapsed) {
const mainIconSize = "h-6 w-6";
return (
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
<div
onClick={(e) => handleMenuItemClick('/settings', e)}
<div
className={`border-t ${
theme === "dark"
? "border-[#1e1e1e] bg-[#121212]"
: "border-sidebar-border bg-sidebar"
} p-4 flex justify-center ${collapsed ? "hidden" : ""}`}
>
<div
onClick={(e) => handleMenuItemClick("/settings", e)}
className="cursor-pointer"
>
<Settings className={`${mainIconSize} text-purple-400`} />
@@ -61,33 +84,59 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
}
return (
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
<CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
<div
className={`flex-1 flex flex-col border-t ${
theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-sidebars"
} p-4 ${collapsed ? "hidden" : ""}`}
>
<Collapsible
open={settingsPanelOpen}
onOpenChange={setSettingsPanelOpen}
className="w-full flex flex-col flex-1"
>
<CollapsibleTrigger
className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${
theme === "dark" ? "hover:bg-[#1a1a1a]" : "hover:bg-sidebar-accent"
}`}
>
<div className="flex items-center">
<span className="font-medium tracking-wide">{t("settingPanel")}</span>
<span className="font-medium tracking-wide">
{t("settingPanel")}
</span>
</div>
<div className="flex items-center">
<Settings className="h-4 w-4 mr-1" />
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
<ChevronDown
className={`h-4 w-4 transition-transform duration-200 ${
settingsPanelOpen ? "rotate-180" : ""
}`}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}>
<CollapsibleContent
className={`${
theme === "dark" ? "bg-black" : "bg-sidebars"
} flex-1 flex flex-col`}
>
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
<ScrollArea className="h-full">
<div className="space-y-2 pr-4">
<div className="space-y-2 pr-4 cursor-pointer">
{settingsMenuItems.map((item) => (
<div
<div
key={item.id}
className={getMenuItemClasses(activeSettingsItem === item.id)}
className={getMenuItemClasses(
activeSettingsItem === item.id
)}
onClick={(e) => {
handleMenuItemClick(`/settings?panel=${item.id}`, e);
handleSettingsItemClick(item.id);
}}
>
<item.icon className="h-4 w-4 mr-2" />
<span className="text-sm">{t(item.translationKey)}</span>
<span className="text-base">{t(item.translationKey)}</span>
</div>
))}
</div>
@@ -97,4 +146,4 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
</Collapsible>
</div>
);
};
};
@@ -1,4 +1,3 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
@@ -10,15 +9,17 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
const { theme } = useTheme();
return (
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
<div className="h-8 w-8 bg-gray-600 rounded flex items-center justify-center mr-2">
<img
src="/favicon_sidebar.ico"
alt="CheckCle"
className="h-6 w-6"
/>
<div
className={`p-[15.7px] ${
theme === "dark"
? "border-[#1e1e1e] bg-black"
: "border-sidebar-border bg-white"
} border-b flex items-center ${collapsed ? "justify-center" : ""}`}
>
<div className="h-8 w-8 rounded flex items-center justify-center mr-2 dark:bg-drak">
<img src="/favicon_sidebar.ico" alt="CheckCle" className="h-7 w-6" />
</div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div>
);
};
};
@@ -1,14 +1,17 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage';
import { CreateOperationalPageDialog } from './CreateOperationalPageDialog';
import { EditOperationalPageDialog } from './EditOperationalPageDialog';
import { OperationalPageCard } from './OperationalPageCard';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { OperationalPageRecord } from '@/types/operational.types';
import { Activity, Plus, RefreshCw } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
useOperationalPages,
useDeleteOperationalPage,
} from "@/hooks/useOperationalPage";
import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog";
import { EditOperationalPageDialog } from "./EditOperationalPageDialog";
import { OperationalPageCard } from "./OperationalPageCard";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { OperationalPageRecord } from "@/types/operational.types";
import { Activity, Plus, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import {
AlertDialog,
AlertDialogAction,
@@ -18,17 +21,26 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
} from "@/components/ui/alert-dialog";
export const OperationalPageContent = () => {
const navigate = useNavigate();
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
const {
data: pages,
isLoading,
error,
refetch,
isRefetching,
} = useOperationalPages();
const deleteMutation = useDeleteOperationalPage();
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null);
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(
null
);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pageToDelete, setPageToDelete] = useState<OperationalPageRecord | null>(null);
const [pageToDelete, setPageToDelete] =
useState<OperationalPageRecord | null>(null);
const handleEdit = (page: OperationalPageRecord) => {
setEditingPage(page);
@@ -37,10 +49,10 @@ export const OperationalPageContent = () => {
const handleView = (page: OperationalPageRecord) => {
if (page.custom_domain) {
window.open(`https://${page.custom_domain}`, '_blank');
window.open(`https://${page.custom_domain}`, "_blank");
} else {
// Navigate to the public status page route using the correct format
window.open(`/public/${page.slug}`, '_blank');
window.open(`/public/${page.slug}`, "_blank");
}
};
@@ -56,7 +68,7 @@ export const OperationalPageContent = () => {
setDeleteDialogOpen(false);
setPageToDelete(null);
} catch (error) {
console.error('Error deleting page:', error);
console.error("Error deleting page:", error);
}
}
};
@@ -68,7 +80,9 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">Failed to load operational pages</h3>
<h3 className="text-lg font-semibold mb-2">
Failed to load operational pages
</h3>
<p className="text-muted-foreground mb-4">
There was an error loading your operational pages. Please try again.
</p>
@@ -82,16 +96,18 @@ export const OperationalPageContent = () => {
}
return (
<div className="container mx-auto px-4 py-8">
<div className="w-full container mx-auto px-4 py-8 ">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8 ">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">Operational Pages</h1>
<h1 className="text-3xl font-bold tracking-tight mb-2">
Operational Pages
</h1>
<p className="text-muted-foreground">
Manage your public status pages and monitor service health
</p>
</div>
<div className="flex items-center gap-2 mt-4 sm:mt-0">
<Button
variant="outline"
@@ -99,7 +115,9 @@ export const OperationalPageContent = () => {
onClick={() => refetch()}
disabled={isRefetching}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`} />
<RefreshCw
className={`h-4 w-4 mr-2 ${isRefetching ? "animate-spin" : ""}`}
/>
Refresh
</Button>
<CreateOperationalPageDialog />
@@ -135,9 +153,12 @@ export const OperationalPageContent = () => {
<div className="mb-4">
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
</div>
<h3 className="text-lg font-semibold mb-2">No operational pages found</h3>
<h3 className="text-lg font-semibold mb-2">
No operational pages found
</h3>
<p className="text-muted-foreground mb-6">
Create your first operational page to start monitoring your services and communicate status to your users.
Create your first operational page to start monitoring your
services and communicate status to your users.
</p>
<CreateOperationalPageDialog />
</CardContent>
@@ -167,12 +188,12 @@ export const OperationalPageContent = () => {
<span className="font-medium">Total Pages:</span> {pages.length}
</div>
<div>
<span className="font-medium">Public Pages:</span>{' '}
{pages.filter(p => p.is_public === 'true').length}
<span className="font-medium">Public Pages:</span>{" "}
{pages.filter((p) => p.is_public === "true").length}
</div>
<div>
<span className="font-medium">Operational:</span>{' '}
{pages.filter(p => p.status === 'operational').length}
<span className="font-medium">Operational:</span>{" "}
{pages.filter((p) => p.status === "operational").length}
</div>
</div>
</div>
@@ -191,7 +212,9 @@ export const OperationalPageContent = () => {
<AlertDialogHeader>
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone and will permanently remove the operational page and all its components.
Are you sure you want to delete "{pageToDelete?.title}"? This
action cannot be undone and will permanently remove the
operational page and all its components.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -201,11 +224,11 @@ export const OperationalPageContent = () => {
className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
{deleteMutation.isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
};
@@ -1,8 +1,13 @@
import React, { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
import { regionalService } from "@/services/regionalService";
import { RegionalService } from "@/types/regional.types";
@@ -15,24 +20,29 @@ export const RegionalMonitoringContent = () => {
const { toast } = useToast();
const queryClient = useQueryClient();
const { data: regionalServices = [], isLoading, error } = useQuery({
queryKey: ['regional-services'],
const {
data: regionalServices = [],
isLoading,
error,
} = useQuery({
queryKey: ["regional-services"],
queryFn: regionalService.getRegionalServices,
refetchInterval: 30000, // Refetch every 30 seconds
});
const handleAgentAdded = () => {
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
toast({
title: "Regional Agent Added",
description: "The regional monitoring agent has been successfully configured.",
description:
"The regional monitoring agent has been successfully configured.",
});
};
const handleDeleteAgent = async (id: string) => {
try {
await regionalService.deleteRegionalService(id);
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
toast({
title: "Agent Removed",
description: "The regional monitoring agent has been removed.",
@@ -46,14 +56,18 @@ export const RegionalMonitoringContent = () => {
}
};
const onlineAgents = regionalServices.filter(agent => agent.connection === 'online').length;
const onlineAgents = regionalServices.filter(
(agent) => agent.connection === "online"
).length;
const totalAgents = regionalServices.length;
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 ">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
<h1 className="text-3xl font-bold tracking-tight">
Regional Monitoring
</h1>
<p className="text-muted-foreground">
Manage distributed monitoring agents across different regions
</p>
@@ -85,23 +99,25 @@ export const RegionalMonitoringContent = () => {
<Wifi className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
<p className="text-xs text-muted-foreground">
Currently connected
</p>
<div className="text-2xl font-bold text-green-600">
{onlineAgents}
</div>
<p className="text-xs text-muted-foreground">Currently connected</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Offline Agents</CardTitle>
<CardTitle className="text-sm font-medium">
Offline Agents
</CardTitle>
<WifiOff className="h-4 w-4 text-red-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
<p className="text-xs text-muted-foreground">
Disconnected agents
</p>
<div className="text-2xl font-bold text-red-600">
{totalAgents - onlineAgents}
</div>
<p className="text-xs text-muted-foreground">Disconnected agents</p>
</CardContent>
</Card>
</div>
@@ -109,7 +125,7 @@ export const RegionalMonitoringContent = () => {
{/* Agents List */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Regional Agents</h2>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, i) => (
@@ -133,7 +149,8 @@ export const RegionalMonitoringContent = () => {
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
<p className="text-muted-foreground text-center mb-4">
Get started by adding your first regional monitoring agent to extend your monitoring coverage.
Get started by adding your first regional monitoring agent to
extend your monitoring coverage.
</p>
<Button onClick={() => setAddDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
@@ -144,9 +161,9 @@ export const RegionalMonitoringContent = () => {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{regionalServices.map((agent) => (
<RegionalAgentCard
key={agent.id}
agent={agent}
<RegionalAgentCard
key={agent.id}
agent={agent}
onDelete={() => handleDeleteAgent(agent.id)}
/>
))}
@@ -161,4 +178,4 @@ export const RegionalMonitoringContent = () => {
/>
</div>
);
};
};
@@ -1,33 +1,37 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Plus, CalendarClock, AlertCircle } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
import { IncidentManagementTab } from "./IncidentManagementTab";
import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
import { useToast } from '@/hooks/use-toast';
import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
import { CreateMaintenanceDialog } from "./maintenance/CreateMaintenanceDialog";
import { CreateIncidentDialog } from "./incident/CreateIncidentDialog";
import { useToast } from "@/hooks/use-toast";
import {
initMaintenanceNotifications,
stopMaintenanceNotifications,
} from "@/services/maintenance/maintenanceNotificationService";
export const ScheduleIncidentContent = () => {
const { t } = useLanguage();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("maintenance");
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] =
useState(false);
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] =
useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
// Initialize maintenance notifications when the component mounts
useEffect(() => {
// console.log("Initializing maintenance notifications");
// console.log("Initializing maintenance notifications");
initMaintenanceNotifications();
// Clean up when the component unmounts
return () => {
// console.log("Cleaning up maintenance notifications");
// console.log("Cleaning up maintenance notifications");
stopMaintenanceNotifications();
};
}, []);
@@ -43,65 +47,67 @@ export const ScheduleIncidentContent = () => {
const handleMaintenanceCreated = () => {
// Refresh data by incrementing the refresh trigger
const newTriggerValue = refreshTrigger + 1;
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
// console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
setRefreshTrigger(newTriggerValue);
// Show success toast
toast({
title: t('success'),
description: t('maintenanceCreatedSuccess'),
title: t("success"),
description: t("maintenanceCreatedSuccess"),
});
};
const handleIncidentCreated = () => {
// Refresh data by incrementing the refresh trigger
const newTriggerValue = incidentRefreshTrigger + 1;
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
// console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
setIncidentRefreshTrigger(newTriggerValue);
// Show success toast
toast({
title: t('success'),
description: t('incidentCreatedSuccess'),
title: t("success"),
description: t("incidentCreatedSuccess"),
});
};
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6">
<div className="flex flex-col gap-6">
<main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 ">
<div className="flex flex-col gap-6 bg-white dark:bg-black p-3 h-full rounded-xl">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-foreground">
{t('scheduleIncidentManagement')}
{t("scheduleIncidentManagement")}
</h2>
<Button
<Button
className="text-primary-foreground"
onClick={handleCreateButtonClick}
>
<Plus className="w-4 h-4 mr-2" />
{activeTab === "maintenance" ? t('createMaintenanceWindow') : t('createIncident')}
<Plus className="w-4 h-4 mr-2" />
{activeTab === "maintenance"
? t("createMaintenanceWindow")
: t("createIncident")}
</Button>
</div>
<Tabs
defaultValue="maintenance"
<Tabs
defaultValue="maintenance"
className="w-full"
onValueChange={(value) => setActiveTab(value)}
>
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="maintenance">
<CalendarClock className="w-4 h-4 mr-2" />
{t('scheduledMaintenance')}
{t("scheduledMaintenance")}
</TabsTrigger>
<TabsTrigger value="incidents">
<AlertCircle className="w-4 h-4 mr-2" />
{t('incidentManagement')}
{t("incidentManagement")}
</TabsTrigger>
</TabsList>
<TabsContent value="maintenance" className="space-y-4">
<ScheduledMaintenanceTab refreshTrigger={refreshTrigger} />
</TabsContent>
<TabsContent value="incidents" className="space-y-4">
<IncidentManagementTab refreshTrigger={incidentRefreshTrigger} />
</TabsContent>
@@ -109,14 +115,14 @@ export const ScheduleIncidentContent = () => {
</div>
{/* Maintenance creation dialog */}
<CreateMaintenanceDialog
<CreateMaintenanceDialog
open={createMaintenanceDialogOpen}
onOpenChange={setCreateMaintenanceDialogOpen}
onMaintenanceCreated={handleMaintenanceCreated}
/>
{/* Incident creation dialog */}
<CreateIncidentDialog
<CreateIncidentDialog
open={createIncidentDialogOpen}
onOpenChange={setCreateIncidentDialogOpen}
onIncidentCreated={handleIncidentCreated}
+265 -105
View File
@@ -1,13 +1,45 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
RefreshCw,
Search,
Eye,
Activity,
MoreHorizontal,
Pause,
Play,
Edit,
Trash2,
} from "lucide-react";
import { Server } from "@/types/server.types";
import { ServerStatusBadge } from "./ServerStatusBadge";
import { OSTypeIcon } from "./OSTypeIcon";
@@ -23,7 +55,11 @@ interface ServerTableProps {
onRefresh: () => void;
}
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
export const ServerTable = ({
servers,
isLoading,
onRefresh,
}: ServerTableProps) => {
const { theme } = useTheme();
const [searchTerm, setSearchTerm] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -34,10 +70,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const navigate = useNavigate();
const { toast } = useToast();
const filteredServers = servers.filter(server =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
const filteredServers = servers.filter(
(server) =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleViewDetails = (serverId: string) => {
@@ -51,41 +88,44 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const handlePauseResume = async (server: Server) => {
const serverId = server.id;
const isPaused = server.status === "paused";
if (pausingServers.has(serverId)) {
return; // Already processing this server
}
try {
setPausingServers(prev => new Set(prev).add(serverId));
setPausingServers((prev) => new Set(prev).add(serverId));
// Only update the status field, preserving all other server configuration
const updateData = {
status: isPaused ? "up" : "paused",
last_checked: new Date().toISOString()
last_checked: new Date().toISOString(),
};
await pb.collection('servers').update(serverId, updateData);
await pb.collection("servers").update(serverId, updateData);
toast({
title: isPaused ? "Server resumed" : "Server paused",
description: `Monitoring ${isPaused ? 'resumed' : 'paused'} for ${server.name}`,
description: `Monitoring ${isPaused ? "resumed" : "paused"} for ${
server.name
}`,
});
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
// console.log(`${isPaused ? 'Resume' : 'Pause'} server monitoring: ${serverId}`);
// Refresh the server list to show updated status
onRefresh();
} catch (error) {
// console.error('Error updating server status:', error);
// console.error('Error updating server status:', error);
toast({
variant: "destructive",
title: "Error",
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
description: `Failed to ${
isPaused ? "resume" : "pause"
} server monitoring. Please try again.`,
});
} finally {
setPausingServers(prev => {
setPausingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverId);
return newSet;
@@ -108,24 +148,23 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
try {
setIsDeleting(true);
// Delete the server from the database
await pb.collection('servers').delete(selectedServer.id);
await pb.collection("servers").delete(selectedServer.id);
toast({
title: "Server deleted",
description: `${selectedServer.name} has been deleted successfully.`,
});
// Refresh the server list
onRefresh();
// Close the dialog
setDeleteDialogOpen(false);
setSelectedServer(null);
} catch (error) {
// console.error('Error deleting server:', error);
// console.error('Error deleting server:', error);
toast({
variant: "destructive",
title: "Error",
@@ -136,65 +175,65 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
}
};
const CustomProgressBar = ({
value,
label,
subtitle,
type
}: {
value: number;
label: string;
subtitle: string;
type: 'cpu' | 'memory' | 'disk'
const CustomProgressBar = ({
value,
label,
subtitle,
type,
}: {
value: number;
label: string;
subtitle: string;
type: "cpu" | "memory" | "disk";
}) => {
const getGradientColors = (type: string, value: number) => {
if (type === 'cpu') {
if (value > 90) return 'from-red-500 to-red-600';
if (value > 75) return 'from-orange-500 to-orange-600';
if (value > 60) return 'from-yellow-500 to-yellow-600';
return 'from-green-500 to-green-600';
if (type === "cpu") {
if (value > 90) return "from-red-500 to-red-600";
if (value > 75) return "from-orange-500 to-orange-600";
if (value > 60) return "from-yellow-500 to-yellow-600";
return "from-green-500 to-green-600";
}
if (type === 'memory') {
if (value > 90) return 'from-red-500 to-red-600';
if (value > 75) return 'from-yellow-500 to-yellow-600';
return 'from-blue-500 to-blue-600';
if (type === "memory") {
if (value > 90) return "from-red-500 to-red-600";
if (value > 75) return "from-yellow-500 to-yellow-600";
return "from-blue-500 to-blue-600";
}
if (type === 'disk') {
if (value > 95) return 'from-red-500 to-red-600';
if (value > 85) return 'from-yellow-500 to-yellow-600';
return 'from-orange-500 to-orange-600';
if (type === "disk") {
if (value > 95) return "from-red-500 to-red-600";
if (value > 85) return "from-yellow-500 to-yellow-600";
return "from-orange-500 to-orange-600";
}
return 'from-gray-500 to-gray-600';
return "from-gray-500 to-gray-600";
};
const getTextColor = (value: number) => {
if (value > 90) return 'text-red-600 dark:text-red-400';
if (value > 75) return 'text-orange-600 dark:text-orange-400';
if (value > 60) return 'text-yellow-600 dark:text-yellow-400';
return 'text-green-600 dark:text-green-400';
if (value > 90) return "text-red-600 dark:text-red-400";
if (value > 75) return "text-orange-600 dark:text-orange-400";
if (value > 60) return "text-yellow-600 dark:text-yellow-400";
return "text-green-600 dark:text-green-400";
};
return (
<div className="space-y-2 min-w-[120px]">
<div className="space-y-2 min-w-[120px] -mt-20">
<div className="flex justify-between items-center">
<span className={`text-sm font-semibold ${getTextColor(value)}`}>
{label}
</span>
<span className="text-xs text-muted-foreground">
{subtitle}
</span>
<span className="text-xs text-muted-foreground">{subtitle} </span>
</div>
<div className="relative">
<div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner">
<div
className={`h-full bg-gradient-to-r ${getGradientColors(type, value)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
<div
className={`h-full bg-gradient-to-r ${getGradientColors(
type,
value
)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
style={{ width: `${Math.min(value, 100)}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse opacity-60" />
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" />
</div>
</div>
</div>
</div>
);
@@ -202,7 +241,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
if (isLoading) {
return (
<Card className="flex-1 flex flex-col">
<Card className="flex-1 flex flex-col ">
<CardHeader className="flex-shrink-0">
<CardTitle>Servers</CardTitle>
</CardHeader>
@@ -244,33 +283,113 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<p className="text-muted-foreground">No servers found</p>
</div>
) : (
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
<div
className={`${
theme === "dark" ? "bg-gray-900" : "bg-white"
} rounded-lg border border-border shadow-sm`}
>
<Table>
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Status</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>OS</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>IP Address</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Last Checked</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>Actions</TableHead>
<TableHeader
className={`${
theme === "dark" ? "bg-gray-800" : "bg-gray-50"
}`}
>
<TableRow
className={`${
theme === "dark"
? "border-gray-700 hover:bg-gray-800"
: "border-gray-200 hover:bg-gray-100"
}`}
>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Name
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Status
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
OS
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
IP Address
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
CPU
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Memory
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Disk
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Uptime
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4`}
>
Last Checked
</TableHead>
<TableHead
className={`${
theme === "dark" ? "text-gray-300" : "text-gray-700"
} font-medium text-base py-4 text-right`}
>
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredServers.map((server) => {
const cpuUsage = server.cpu_usage || 0;
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
const memoryUsage =
server.ram_total > 0
? (server.ram_used / server.ram_total) * 100
: 0;
const diskUsage =
server.disk_total > 0
? (server.disk_used / server.disk_total) * 100
: 0;
const isPaused = server.status === "paused";
const isProcessing = pausingServers.has(server.id);
return (
<TableRow
key={server.id}
<TableRow
key={server.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleViewDetails(server.id)}
>
@@ -285,7 +404,10 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<TableCell>
<div className="flex items-center gap-2">
<OSTypeIcon osType={server.os_type} />
<span className="text-sm truncate" title={server.os_type}>
<span
className="text-sm truncate"
title={server.os_type}
>
{server.os_type}
</span>
</div>
@@ -307,7 +429,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<CustomProgressBar
value={memoryUsage}
label={`${memoryUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes(server.ram_total)}
subtitle={serverService.formatBytes(
server.ram_total
)}
type="memory"
/>
</TableCell>
@@ -315,12 +439,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<CustomProgressBar
value={diskUsage}
label={`${diskUsage.toFixed(1)}%`}
subtitle={serverService.formatBytes(server.disk_total)}
subtitle={serverService.formatBytes(
server.disk_total
)}
type="disk"
/>
</TableCell>
<TableCell>
<div className="text-sm truncate" title={server.uptime}>
<div
className="text-sm truncate"
title={server.uptime}
>
{server.uptime}
</div>
</TableCell>
@@ -329,10 +458,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
{new Date(server.last_checked).toLocaleString()}
</div>
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<TableCell
className="text-right"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
<Button
variant="ghost"
className="h-8 w-8 p-0"
disabled={isProcessing}
>
<span className="sr-only">Open menu</span>
{isProcessing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
@@ -341,20 +477,36 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
<DropdownMenuContent
align="end"
className="w-[200px]"
>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleViewDetails(server.id);
}}
>
<Eye className="mr-2 h-4 w-4" />
View Server Detail
</DropdownMenuItem>
{server.docker === 'true' && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
{server.docker === "true" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleViewContainers(server.id);
}}
>
<Activity className="mr-2 h-4 w-4" />
Container Monitoring
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePauseResume(server);
}}
disabled={isProcessing}
>
{isPaused ? (
@@ -370,12 +522,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleEdit(server);
}}
>
<Edit className="mr-2 h-4 w-4" />
Edit Server
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleDelete(server);
}}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -406,19 +566,19 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
<AlertDialogTitle>
Are you sure you want to delete this server?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete{' '}
This action cannot be undone. This will permanently delete{" "}
<span className="font-semibold text-foreground">
{selectedServer?.name}
</span>{' '}
</span>{" "}
and all of its monitoring data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>
Cancel
</AlertDialogCancel>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={isDeleting}
@@ -2,13 +2,24 @@ import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Plus, RefreshCw } from "lucide-react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
import { SSLCertificatesTable } from "./SSLCertificatesTable";
import { LoadingState } from "@/components/services/LoadingState";
import { fetchSSLCertificates, addSSLCertificate, checkAndUpdateCertificate, refreshAllCertificates, deleteSSLCertificate } from "@/services/ssl";
import {
fetchSSLCertificates,
addSSLCertificate,
checkAndUpdateCertificate,
refreshAllCertificates,
deleteSSLCertificate,
} from "@/services/ssl";
import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
@@ -21,18 +32,23 @@ export const SSLDomainContent = () => {
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [refreshingId, setRefreshingId] = useState<string | null>(null);
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
const [selectedCertificate, setSelectedCertificate] =
useState<SSLCertificate | null>(null);
const queryClient = useQueryClient();
// Fetch SSL certificates with explicit error handling
const { data: certificates = [], isLoading, error } = useQuery({
queryKey: ['ssl-certificates'],
const {
data: certificates = [],
isLoading,
error,
} = useQuery({
queryKey: ["ssl-certificates"],
queryFn: async () => {
try {
const result = await fetchSSLCertificates();
return result;
} catch (error) {
toast.error(t('failedToLoadCertificates'));
toast.error(t("failedToLoadCertificates"));
throw error;
}
},
@@ -44,101 +60,114 @@ export const SSLDomainContent = () => {
const addMutation = useMutation({
mutationFn: addSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
setIsAddDialogOpen(false);
toast.success(t('sslCertificateAdded'));
toast.success(t("sslCertificateAdded"));
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : t('failedToAddCertificate'));
}
toast.error(
error instanceof Error ? error.message : t("failedToAddCertificate")
);
},
});
// Edit certificate mutation - Updated to include notification_id and template_id
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
// Create the update data object with new fields
const updateData = {
warning_threshold: Number(certificate.warning_threshold),
expiry_threshold: Number(certificate.expiry_threshold),
notification_channel: certificate.notification_channel,
notification_id: certificate.notification_id || '', // Multi notification channels
template_id: certificate.template_id || '', // Alert template ID
notification_id: certificate.notification_id || "", // Multi notification channels
template_id: certificate.template_id || "", // Alert template ID
check_interval: certificate.check_interval,
};
// Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
const updated = await pb
.collection("ssl_certificates")
.update(certificate.id, updateData);
// After updating the settings, refresh the certificate to ensure it's up to date
// This will also check if notification needs to be sent based on updated thresholds
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
return refreshedCert;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
setIsEditDialogOpen(false);
setSelectedCertificate(null);
toast.success(t('sslCertificateUpdated'));
toast.success(t("sslCertificateUpdated"));
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
}
toast.error(
error instanceof Error ? error.message : t("failedToUpdateCertificate")
);
},
});
// Delete certificate mutation
const deleteMutation = useMutation({
mutationFn: deleteSSLCertificate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
toast.success(t('sslCertificateDeleted'));
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
toast.success(t("sslCertificateDeleted"));
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
}
toast.error(
error instanceof Error ? error.message : t("failedToDeleteCertificate")
);
},
});
// Refresh certificate mutation - Updated to remove individual toast notifications
const refreshMutation = useMutation({
mutationFn: checkAndUpdateCertificate,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
setRefreshingId(null);
// Removed individual success toast notification
},
onError: (error) => {
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
// Removed individual error toast notification
}
},
});
// Refresh all certificates mutation
const refreshAllMutation = useMutation({
mutationFn: refreshAllCertificates,
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
setIsRefreshingAll(false);
if (result.failed === 0) {
toast.success(t('allCertificatesRefreshed').replace('{count}', result.success.toString()));
toast.success(
t("allCertificatesRefreshed").replace(
"{count}",
result.success.toString()
)
);
} else {
toast.info(t('someCertificatesFailed')
.replace('{success}', result.success.toString())
.replace('{failed}', result.failed.toString()));
toast.info(
t("someCertificatesFailed")
.replace("{success}", result.success.toString())
.replace("{failed}", result.failed.toString())
);
}
},
onError: (error) => {
toast.error(t('failedToCheckCertificate'));
toast.error(t("failedToCheckCertificate"));
setIsRefreshingAll(false);
// Still refresh the data to show any partial information
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
}
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
},
});
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
@@ -166,12 +195,14 @@ export const SSLDomainContent = () => {
const handleRefreshAll = async () => {
if (certificates.length === 0) {
toast.info(t('noCertificatesToRefresh'));
toast.info(t("noCertificatesToRefresh"));
return;
}
setIsRefreshingAll(true);
toast.info(t('startingRefreshAll').replace('{count}', certificates.length.toString()));
toast.info(
t("startingRefreshAll").replace("{count}", certificates.length.toString())
);
refreshAllMutation.mutate();
};
@@ -182,48 +213,60 @@ export const SSLDomainContent = () => {
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>{t('failedToLoadCertificates')}</p>
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>
{t('check')}
<p>{t("failedToLoadCertificates")}</p>
<Button
onClick={() =>
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] })
}
>
{t("check")}
</Button>
</div>
);
}
return (
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<main className="flex-1 flex flex-col overflow-auto bg-background pb-0 dark:bg-black p-2 ">
<div className="flex flex-col flex-1 bg-white dark:bg-black p-6 rounded-xl">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-foreground">{t('sslDomainManagement')}</h2>
<p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p>
<h2 className="text-2xl font-bold text-foreground">
{t("sslDomainManagement")}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t("monitorSSLCertificates")}
</p>
</div>
<div className="flex gap-2">
<Button
<Button
variant="outline"
onClick={handleRefreshAll}
disabled={isRefreshingAll || refreshingId !== null}
className="relative"
>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
{t('refreshAll')}
<RefreshCw
className={`w-4 h-4 mr-2 ${
isRefreshingAll ? "animate-spin" : ""
}`}
/>
{t("refreshAll")}
{isRefreshingAll && (
<span className="absolute top-0 right-0 -mt-2 -mr-2 bg-primary text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
...
</span>
)}
</Button>
<Button
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> {t('addDomain')}
<Plus className="w-4 h-4 mr-2" /> {t("addDomain")}
</Button>
</div>
</div>
<SSLCertificateStatusCards certificates={certificates} />
<div className="mt-6 flex-1 flex flex-col pb-6">
<SSLCertificatesTable />
</div>
@@ -232,26 +275,29 @@ export const SSLDomainContent = () => {
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
<DialogTitle>{t("addSSLCertificate")}</DialogTitle>
</DialogHeader>
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
onCancel={() => setIsAddDialogOpen(false)}
<AddSSLCertificateForm
onSubmit={handleAddCertificate}
onCancel={() => setIsAddDialogOpen(false)}
isPending={addMutation.isPending}
/>
</DialogContent>
</Dialog>
{selectedCertificate && (
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}>
<Dialog
open={isEditDialogOpen}
onOpenChange={(open) => {
setIsEditDialogOpen(open);
if (!open) setSelectedCertificate(null);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
<DialogTitle>{t("editSSLCertificate")}</DialogTitle>
</DialogHeader>
<EditSSLCertificateForm
<EditSSLCertificateForm
certificate={selectedCertificate}
onSubmit={handleUpdateCertificate}
onCancel={() => {
@@ -265,4 +311,4 @@ export const SSLDomainContent = () => {
)}
</main>
);
};
};
@@ -0,0 +1,14 @@
// src/contexts/FontSizeContext.js
import { useEffect } from "react";
const useDynamicFontSize = (size) => {
useEffect(() => {
if (typeof size === "number") {
// Apply font size to the root HTML element
document.documentElement.style.fontSize = `${size}px`;
localStorage.setItem("fontSize", size.toString());
}
}, [size]);
};
export default useDynamicFontSize;
+25 -15
View File
@@ -1,38 +1,48 @@
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext, useState, useEffect } from "react";
type SideBar = boolean;
interface SidebarContextType {
sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void;
sidebarCollapsed: SideBar;
setSidebarCollapsed: React.Dispatch<React.SetStateAction<SideBar>>;
toggleSidebar: () => void;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
interface SidebarProviderProps {
children: React.ReactNode;
}
export const SidebarProvider: React.FC<SidebarProviderProps> = ({
children,
}) => {
// Initialize the state
const [sidebarCollapsed, setSidebarCollapsed] = useState<SideBar>(() => {
const saved = localStorage.getItem("sidebarCollapsed");
return saved === "true";
});
const toggleSidebar = () => {
setSidebarCollapsed(prev => !prev);
setSidebarCollapsed((prev) => {
const newState = !prev;
localStorage.setItem("sidebarCollapsed", newState.toString());
return newState;
});
};
const value = {
sidebarCollapsed,
setSidebarCollapsed,
toggleSidebar
toggleSidebar,
};
return (
<SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
);
};
// A custom hook to consume the sidebar context.
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (context === undefined) {
throw new Error('useSidebar must be used within a SidebarProvider');
throw new Error("useSidebar must be used within a SidebarProvider");
}
return context;
};
};
+54 -40
View File
@@ -1,4 +1,3 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useTheme } from "@/contexts/ThemeContext";
@@ -21,61 +20,69 @@ const InstanceMonitoring = () => {
const { t } = useLanguage();
const { sidebarCollapsed, toggleSidebar } = useSidebar();
const navigate = useNavigate();
const [stats, setStats] = useState<ServerStats>({
total: 0,
online: 0,
offline: 0,
warning: 0
warning: 0,
});
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
const [addDialogOpen, setAddDialogOpen] = useState(false);
const { data: servers = [], isLoading, error, refetch } = useQuery({
queryKey: ['servers'],
const {
data: servers = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ["servers"],
queryFn: serverService.getServers,
refetchInterval: 30000 // Refetch every 30 seconds
refetchInterval: 30000, // Refetch every 30 seconds
});
useEffect(() => {
if (servers.length > 0) {
serverService.getServerStats(servers).then(setStats);
}
}, [servers]);
const handleRefresh = () => {
refetch();
};
const handleLogout = () => {
authService.logout();
navigate('/login');
navigate("/login");
};
const handleAgentAdded = () => {
refetch();
};
if (error) {
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6 ">
<div className="text-center max-w-md w-full">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading servers</h2>
<h2 className="text-xl sm:text-2xl font-bold mb-4">
Error loading servers
</h2>
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
Unable to fetch server data. Please check your connection and try again.
Unable to fetch server data. Please check your connection and
try again.
</p>
<button
onClick={handleRefresh}
<button
onClick={handleRefresh}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base"
>
Retry
@@ -86,22 +93,22 @@ const InstanceMonitoring = () => {
</div>
);
}
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<div className="flex h-screen overflow-hidden bg-background text-foreground ">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
<div className="flex flex-col flex-1 min-w-0 ">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 overflow-auto">
<div className="p-4 sm:p-6 lg:p-8 space-y-6">
<main className="flex-1 overflow-auto dark:bg-black p-2 shadow">
<div className="p-4 sm:p-2 lg:p-2 space-y-6 h-full bg-white dark:bg-black p-4 rounded-xl">
{/* Header Section */}
<div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-2 ">
<div className="min-w-0 flex-1">
<h1 className="text-2xl lg:text-2xl font-bold text-foreground">
Instance Monitoring
@@ -110,7 +117,10 @@ const InstanceMonitoring = () => {
Monitor and manage your server instances in real-time
</p>
</div>
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
<Button
onClick={() => setAddDialogOpen(true)}
className="flex-shrink-0"
>
<Plus className="mr-2 h-4 w-4" />
Add Server Agent
</Button>
@@ -118,13 +128,17 @@ const InstanceMonitoring = () => {
</div>
{/* Stats Cards Section */}
<div>
<div className="p-2 ">
<ServerStatsCards stats={stats} />
</div>
{/* Server Table Section */}
<div>
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
<div className="p-3">
<ServerTable
servers={servers}
isLoading={isLoading}
onRefresh={handleRefresh}
/>
</div>
</div>
</main>
@@ -139,4 +153,4 @@ const InstanceMonitoring = () => {
);
};
export default InstanceMonitoring;
export default InstanceMonitoring;
+46 -36
View File
@@ -1,4 +1,3 @@
import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { authService } from "@/services/authService";
@@ -7,32 +6,41 @@ import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
import { LoadingState } from "@/components/services/LoadingState";
import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
import {
fetchSSLCertificates,
shouldRunDailyCheck,
checkAllCertificatesAndNotify,
} from "@/services/sslCertificateService";
import { useLanguage } from "@/contexts/LanguageContext";
import { useSidebar } from "@/contexts/SidebarContext";
const SslDomain = () => {
// Get language context for translations
const { t } = useLanguage();
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
const navigate = useNavigate();
// Fetch SSL certificates with error handling and debugging
const { data: certificates = [], isLoading, error, refetch } = useQuery({
queryKey: ['ssl-certificates'],
const {
data: certificates = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ["ssl-certificates"],
queryFn: async () => {
// console.log("Fetching SSL certificates from SslDomain page...");
// console.log("Fetching SSL certificates from SslDomain page...");
try {
const result = await fetchSSLCertificates();
// console.log("SSL certificates fetch successful, count:", result.length);
// console.log("SSL certificates fetch successful, count:", result.length);
return result;
} catch (err) {
// console.error("Error fetching SSL certificates from page:", err);
// console.error("Error fetching SSL certificates from page:", err);
throw err;
}
},
@@ -40,23 +48,23 @@ const SslDomain = () => {
refetchInterval: 300000, // Refresh every 5 minutes
retry: 3, // Retry failed requests 3 times
});
// Check all SSL certificates once per day
useEffect(() => {
const checkCertificates = async () => {
// Check if we should run daily check
if (shouldRunDailyCheck()) {
// console.log("Running daily SSL certificate check...");
// console.log("Running daily SSL certificate check...");
await checkAllCertificatesAndNotify();
// Refresh certificate list after daily check
refetch();
}
};
// Run check when component mounts
checkCertificates();
}, [refetch]);
// Handle logout
const handleLogout = () => {
authService.logout();
@@ -69,11 +77,11 @@ const SslDomain = () => {
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<LoadingState />
</div>
@@ -86,23 +94,25 @@ const SslDomain = () => {
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
<div className="flex flex-col flex-1 ">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<div className="flex flex-col items-center justify-center h-full p-6">
<h2 className="text-xl font-bold mb-2">{t('failedToLoadCertificates')}</h2>
<div className="flex flex-col items-center justify-center h-full p-6 ">
<h2 className="text-xl font-bold mb-2">
{t("failedToLoadCertificates")}
</h2>
<p className="text-muted-foreground mb-4">
{error instanceof Error ? error.message : t('unknown')}
{error instanceof Error ? error.message : t("unknown")}
</p>
<button
<button
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
onClick={() => refetch()}
>
{t('check')}
{t("check")}
</button>
</div>
</div>
@@ -114,12 +124,12 @@ const SslDomain = () => {
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
<div className="flex flex-col flex-1 ">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<SSLDomainContent />
</div>
@@ -127,4 +137,4 @@ const SslDomain = () => {
);
};
export default SslDomain;
export default SslDomain;