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:
+24
-20
@@ -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>
|
||||
Generated
+184
@@ -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",
|
||||
|
||||
@@ -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
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user