Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bda862e4d3 | |||
| 6887dc291b | |||
| 29daddfc3b | |||
| 9c44ba6d7f | |||
| c61a740725 | |||
| a9d3d86394 | |||
| 9b0b2663ec | |||
| 0b04034141 | |||
| 0a635ab3aa | |||
| 175d2b6329 | |||
| 3d6c4151b8 | |||
| 5f893e3b20 | |||
| 1296cf6a32 | |||
| 31e5335aac | |||
| b6d4ca5269 | |||
| 7e7522608a | |||
| 43957f65d2 | |||
| 09ece0a77e | |||
| 06ba38aa15 | |||
| abd878fe6c | |||
| f4d47bbd45 | |||
| b675fddc92 | |||
| 0a70ed2143 | |||
| 4e07279a54 | |||
| 020cdec912 | |||
| 4771b2ead0 | |||
| 87c829e100 | |||
| e6093f9fe2 | |||
| 956786a859 | |||
| 6a0169e4d8 | |||
| 92959945f3 | |||
| b2269187d8 | |||
| 05f46969c4 | |||
| ab168b440e | |||
| 2e866d7a95 | |||
| b372131978 |
@@ -85,7 +85,8 @@ body:
|
|||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
label: Checklist
|
label: Checklist
|
||||||
description: Please confirm the following before submitting:
|
description: |
|
||||||
|
Please confirm the following before submitting:
|
||||||
options:
|
options:
|
||||||
- label: I have searched existing issues to avoid creating duplicates.
|
- label: I have searched existing issues to avoid creating duplicates.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ body:
|
|||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Checklist
|
label: Checklist
|
||||||
description: Before submitting, please confirm:
|
description: |
|
||||||
|
Before submitting, please confirm:
|
||||||
options:
|
options:
|
||||||
- label: I have searched existing issues to make sure this feature hasn’t been requested yet.
|
- label: I have searched existing issues to make sure this feature hasn’t been requested yet.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ body:
|
|||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Checklist
|
label: Checklist
|
||||||
description: Before submitting, please confirm the following:
|
description: |
|
||||||
|
Before submitting, please confirm the following:
|
||||||
options:
|
options:
|
||||||
- label: I have searched existing issues and discussions for a solution.
|
- label: I have searched existing issues and discussions for a solution.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
+24
-20
@@ -1,25 +1,29 @@
|
|||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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" />
|
<head>
|
||||||
<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 charset="UTF-8" />
|
||||||
<meta property="og:type" content="website" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
<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 property="og:title" content="checkcle-an-open-source" />
|
||||||
<meta name="twitter:site" content="@checkcle_oss" />
|
<meta property="og:description"
|
||||||
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
|
||||||
</head>
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
||||||
|
|
||||||
<body>
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<div id="root"></div>
|
<meta name="twitter:site" content="@checkcle_oss" />
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
|
||||||
</body>
|
|
||||||
</html>
|
</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",
|
"name": "vite_react_shadcn_ts",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "2.2.7",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
@@ -644,6 +645,21 @@
|
|||||||
"@floating-ui/utils": "^0.2.8"
|
"@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": {
|
"node_modules/@floating-ui/react-dom": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
"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==",
|
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@hookform/resolvers": {
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz",
|
||||||
@@ -2280,6 +2316,103 @@
|
|||||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.20.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
|
||||||
@@ -2729,6 +2862,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/@swc/types": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz",
|
||||||
@@ -2793,6 +2935,33 @@
|
|||||||
"react": "^18 || ^19"
|
"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": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
@@ -6289,6 +6458,12 @@
|
|||||||
"node": ">=12.0.0"
|
"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": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "2.2.7",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
|
"framer-motion": "12.23.12",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
|||||||
@@ -154,7 +154,8 @@
|
|||||||
"email",
|
"email",
|
||||||
"ntfy",
|
"ntfy",
|
||||||
"pushover",
|
"pushover",
|
||||||
"notifiarr"
|
"notifiarr",
|
||||||
|
"gotify"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -339,6 +340,20 @@
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3401535476",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "server_url",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "autodate2990389176",
|
"id": "autodate2990389176",
|
||||||
@@ -5643,4 +5658,4 @@
|
|||||||
"indexes": [],
|
"indexes": [],
|
||||||
"system": false
|
"system": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+139
-45
@@ -1,37 +1,40 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import { LanguageProvider } from './contexts/LanguageContext';
|
import { LanguageProvider } from "./contexts/LanguageContext";
|
||||||
import { SidebarProvider } from './contexts/SidebarContext';
|
import { SidebarProvider } from "./contexts/SidebarContext";
|
||||||
|
|
||||||
import Index from './pages/Index';
|
import Index from "./pages/Index";
|
||||||
import Login from './pages/Login';
|
import Login from "./pages/Login";
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from "./pages/Dashboard";
|
||||||
import InstanceMonitoring from './pages/InstanceMonitoring';
|
import InstanceMonitoring from "./pages/InstanceMonitoring";
|
||||||
import ContainerMonitoring from './pages/ContainerMonitoring';
|
import ContainerMonitoring from "./pages/ContainerMonitoring";
|
||||||
import ServiceDetail from './pages/ServiceDetail';
|
import ServiceDetail from "./pages/ServiceDetail";
|
||||||
import SslDomain from './pages/SslDomain';
|
import SslDomain from "./pages/SslDomain";
|
||||||
import ScheduleIncident from './pages/ScheduleIncident';
|
import ScheduleIncident from "./pages/ScheduleIncident";
|
||||||
import OperationalPage from './pages/OperationalPage';
|
import OperationalPage from "./pages/OperationalPage";
|
||||||
import RegionalMonitoring from './pages/RegionalMonitoring';
|
import RegionalMonitoring from "./pages/RegionalMonitoring";
|
||||||
import Settings from './pages/Settings';
|
import Settings from "./pages/Settings";
|
||||||
import Profile from './pages/Profile';
|
import Profile from "./pages/Profile";
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from "./pages/NotFound";
|
||||||
import PublicStatusPage from './pages/PublicStatusPage';
|
import PublicStatusPage from "./pages/PublicStatusPage";
|
||||||
import { ProtectedRoute } from './components/auth/ProtectedRoute';
|
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
|
||||||
import ServerDetail from './pages/ServerDetail';
|
import ServerDetail from "./pages/ServerDetail";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [queryClient] = useState(() => new QueryClient({
|
const [queryClient] = useState(
|
||||||
defaultOptions: {
|
() =>
|
||||||
queries: {
|
new QueryClient({
|
||||||
staleTime: 60 * 1000
|
defaultOptions: {
|
||||||
}
|
queries: {
|
||||||
}
|
staleTime: 60 * 1000,
|
||||||
}));
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -43,22 +46,113 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
{/* Public routes */}
|
{/* Public routes */}
|
||||||
<Route path="/public/:slug" element={<PublicStatusPage />} />
|
<Route path="/public/:slug" element={<PublicStatusPage />} />
|
||||||
|
|
||||||
{/* Protected routes */}
|
{/* Protected routes */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} />
|
<Route
|
||||||
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
path="/"
|
||||||
<Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} />
|
element={
|
||||||
<Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} />
|
<ProtectedRoute>
|
||||||
<Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
|
<Index />
|
||||||
<Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
|
</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
|
||||||
<Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} />
|
path="/dashboard"
|
||||||
<Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} />
|
element={
|
||||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
<ProtectedRoute>
|
||||||
<Route path="/profile" element={<ProtectedRoute><Profile /></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 />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
@@ -69,4 +163,4 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
@@ -15,17 +14,24 @@ interface DashboardContentProps {
|
|||||||
error: Error | null;
|
error: Error | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
|
export const DashboardContent = ({
|
||||||
|
services,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
}: DashboardContentProps) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [filter, setFilter] = useState<string>("all");
|
const [filter, setFilter] = useState<string>("all");
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Filter services based on search term and type filter
|
// Filter services based on search term and type filter
|
||||||
const filteredServices = services.filter(service => {
|
const filteredServices = services.filter((service) => {
|
||||||
const matchesSearch = service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const matchesSearch =
|
||||||
(service.url && service.url.toLowerCase().includes(searchTerm.toLowerCase()));
|
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
const matchesFilter = filter === 'all' || service.type.toLowerCase() === filter.toLowerCase();
|
(service.url &&
|
||||||
|
service.url.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
const matchesFilter =
|
||||||
|
filter === "all" || service.type.toLowerCase() === filter.toLowerCase();
|
||||||
return matchesSearch && matchesFilter;
|
return matchesSearch && matchesFilter;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,40 +39,42 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
||||||
<p>Error loading service data.</p>
|
<p>Error loading service data.</p>
|
||||||
<Button onClick={() => window.location.reload()}>{t('retry')}</Button>
|
<Button onClick={() => window.location.reload()}>{t("retry")}</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
<main className="flex-1 flex flex-col overflow-auto dark:bg-black p-2 shadow pb-0 ">
|
||||||
<div className="flex flex-col flex-1">
|
<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">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-2xl font-bold text-foreground">{t('overview')}</h2>
|
<h2 className="text-2xl font-bold text-foreground">
|
||||||
<Button
|
{t("overview")}
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
className="text-primary-foreground"
|
className="text-primary-foreground"
|
||||||
onClick={() => setIsAddDialogOpen(true)}
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" /> {t('newService')}
|
<Plus className="w-4 h-4 mr-2" /> {t("newService")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusCards services={services} />
|
<StatusCards services={services} />
|
||||||
|
|
||||||
<ServiceFilters
|
<ServiceFilters
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
setSearchTerm={setSearchTerm}
|
setSearchTerm={setSearchTerm}
|
||||||
servicesCount={filteredServices.length}
|
servicesCount={filteredServices.length}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col pb-6">
|
<div className="flex-1 flex flex-col pb-6">
|
||||||
<ServicesTable services={filteredServices} />
|
<ServicesTable services={filteredServices} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddServiceDialog
|
<AddServiceDialog
|
||||||
open={isAddDialogOpen}
|
open={isAddDialogOpen}
|
||||||
onOpenChange={setIsAddDialogOpen}
|
onOpenChange={setIsAddDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,39 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AuthUser } from "@/services/authService";
|
import { AuthUser } from "@/services/authService";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
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 { 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 { useEffect, useState } from "react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||||
|
import useDynamicFontSize from "@/contexts/FontSizeContext";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
currentUser: AuthUser | null;
|
currentUser: AuthUser | null;
|
||||||
@@ -20,7 +46,7 @@ export const Header = ({
|
|||||||
currentUser,
|
currentUser,
|
||||||
onLogout,
|
onLogout,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
toggleSidebar
|
toggleSidebar,
|
||||||
}: HeaderProps) => {
|
}: HeaderProps) => {
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
const { language, setLanguage, t } = useLanguage();
|
const { language, setLanguage, t } = useLanguage();
|
||||||
@@ -55,148 +81,270 @@ export const Header = ({
|
|||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
|
// Prepare avatar URL - ensure it displays correctly if it's a local profile image
|
||||||
let avatarUrl = '';
|
let avatarUrl = "";
|
||||||
if (currentUser?.avatar) {
|
if (currentUser?.avatar) {
|
||||||
// If it's a relative path from the public folder, make sure it's resolved properly
|
// 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;
|
avatarUrl = currentUser.avatar;
|
||||||
} else {
|
} else {
|
||||||
avatarUrl = currentUser.avatar;
|
avatarUrl = currentUser.avatar;
|
||||||
}
|
}
|
||||||
console.log("Final avatar URL:", avatarUrl);
|
console.log("Final avatar URL:", avatarUrl);
|
||||||
}
|
}
|
||||||
|
const [fontSize, setFontSize] = useState(() => {
|
||||||
|
const savedFontSize = localStorage.getItem("fontSize");
|
||||||
|
return savedFontSize ? parseInt(savedFontSize, 10) : 14;
|
||||||
|
});
|
||||||
|
useDynamicFontSize(fontSize);
|
||||||
return (
|
return (
|
||||||
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden">
|
<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 */}
|
{/* Grid Pattern Overlay - Similar to StatusCards */}
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<div
|
<div
|
||||||
className="w-full h-full"
|
className="w-full h-full dark:bg-black"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px),
|
backgroundImage: `linear-gradient(${
|
||||||
linear-gradient(90deg, ${theme === 'dark' ? '#ffffff10' : '#00000010'} 1px, transparent 1px)`,
|
theme === "dark" ? "#ffffff10" : "#00000010"
|
||||||
backgroundSize: '20px 20px'
|
} 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 className="w-full h-full backdrop-blur-[1px]"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Content */}
|
{/* Header Content */}
|
||||||
<div className="flex items-center gap-4 z-10">
|
<div className="flex items-center gap-4 z-10">
|
||||||
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
|
<div
|
||||||
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
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>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 z-10">
|
<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>
|
<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>
|
</Button>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
<span className="sr-only">{t("language")}</span>
|
||||||
<Globe className="w-4 h-4" />
|
<Globe className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => setLanguage("en")}
|
||||||
|
className={language === "en" ? "bg-accent" : ""}
|
||||||
|
>
|
||||||
{t("english")}
|
{t("english")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setLanguage("km")} className={language === "km" ? "bg-accent" : ""}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => setLanguage("km")}
|
||||||
|
className={language === "km" ? "bg-accent" : ""}
|
||||||
|
>
|
||||||
{t("khmer")}
|
{t("khmer")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => setLanguage("de")}
|
||||||
|
className={language === "de" ? "bg-accent" : ""}
|
||||||
|
>
|
||||||
{t("Deutsch")}
|
{t("Deutsch")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => setLanguage("ko")}
|
||||||
|
className={language === "ko" ? "bg-accent" : ""}
|
||||||
|
>
|
||||||
{t("korean")}
|
{t("korean")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => setLanguage("ja")}
|
||||||
|
className={language === "ja" ? "bg-accent" : ""}
|
||||||
|
>
|
||||||
{t("japanese")}
|
{t("japanese")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setLanguage("zhcn")} className={language === "zhcn" ? "bg-accent" : ""}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => setLanguage("zhcn")}
|
||||||
|
className={language === "zhcn" ? "bg-accent" : ""}
|
||||||
|
>
|
||||||
{t("simplifiedChinese")}
|
{t("simplifiedChinese")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{/* Documentation */}
|
{/* Documentation */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-full w-8 h-8 border-border"
|
className="rounded-full w-8 h-8 border-border"
|
||||||
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
|
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t("documentation")}</span>
|
<span className="sr-only">{t("documentation")}</span>
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* GitHub */}
|
{/* GitHub */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-full w-8 h-8 border-border"
|
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>
|
<span className="sr-only">GitHub</span>
|
||||||
<Github className="w-4 h-4" />
|
<Github className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* X (Twitter) */}
|
{/* X (Twitter) */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-full w-8 h-8 border-border"
|
className="rounded-full w-8 h-8 border-border"
|
||||||
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
|
onClick={() => window.open("https://x.com/checkcle_oss)", "_blank")}
|
||||||
>
|
>
|
||||||
<span className="sr-only">X (Twitter)</span>
|
<span className="sr-only">X (Twitter)</span>
|
||||||
<Twitter className="w-4 h-4" />
|
<Twitter className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Discord */}
|
{/* Discord */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-full w-8 h-8 border-border"
|
className="rounded-full w-8 h-8 border-border"
|
||||||
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
|
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Discord</span>
|
<span className="sr-only">Discord</span>
|
||||||
<MessageSquare className="w-4 h-4" />
|
<MessageSquare className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* 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>
|
<span className="sr-only">{t("notifications")}</span>
|
||||||
<Bell className="w-4 h-4" />
|
<Bell className="w-4 h-4" />
|
||||||
</Button>
|
</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 */}
|
{/* User Profile Dropdown */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground">
|
<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">
|
{avatarUrl ? (
|
||||||
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
|
<AvatarImage
|
||||||
</AvatarFallback>}
|
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>
|
</Avatar>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
<div className="flex items-center gap-3 p-2">
|
<div className="flex items-center gap-3 p-2">
|
||||||
<Avatar className="h-10 w-10">
|
<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">
|
{avatarUrl ? (
|
||||||
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
|
<AvatarImage
|
||||||
</AvatarFallback>}
|
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>
|
</Avatar>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<span className="font-medium">{currentUser?.name || 'User'}</span>
|
<span className="font-medium">
|
||||||
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
|
{currentUser?.name || "User"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
|
{currentUser?.email}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -209,7 +357,10 @@ export const Header = ({
|
|||||||
<span>{t("settings")}</span>
|
<span>{t("settings")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<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" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
<span>{t("logout")}</span>
|
<span>{t("logout")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { Plus } from "lucide-react";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
|
||||||
@@ -13,29 +18,31 @@ interface ServiceFiltersProps {
|
|||||||
servicesCount: number;
|
servicesCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceFilters = ({
|
export const ServiceFilters = ({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
servicesCount
|
servicesCount,
|
||||||
}: ServiceFiltersProps) => {
|
}: ServiceFiltersProps) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex justify-between items-center">
|
<div className="mb-6 flex justify-between items-center ">
|
||||||
<div className="flex 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">
|
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
|
||||||
{servicesCount}
|
{servicesCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<Select value={filter} onValueChange={setFilter}>
|
<Select value={filter} onValueChange={setFilter}>
|
||||||
<SelectTrigger className="w-40 bg-card border-border">
|
<SelectTrigger className="w-40 bg-card border-border dark:bg-gray-800">
|
||||||
<SelectValue placeholder={t('allTypes')} />
|
<SelectValue placeholder={t("allTypes")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">{t('allTypes')}</SelectItem>
|
<SelectItem value="all">{t("allTypes")}</SelectItem>
|
||||||
<SelectItem value="HTTP">HTTP</SelectItem>
|
<SelectItem value="HTTP">HTTP</SelectItem>
|
||||||
<SelectItem value="PING">PING</SelectItem>
|
<SelectItem value="PING">PING</SelectItem>
|
||||||
<SelectItem value="TCP">TCP</SelectItem>
|
<SelectItem value="TCP">TCP</SelectItem>
|
||||||
@@ -43,9 +50,9 @@ export const ServiceFilters = ({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className="w-72 bg-card border-border"
|
className="w-72 bg-card border-border"
|
||||||
placeholder={t('search')}
|
placeholder={t("search")}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { SidebarHeader } from "./sidebar/SidebarHeader";
|
import { SidebarHeader } from "./sidebar/SidebarHeader";
|
||||||
@@ -11,18 +10,25 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export const Sidebar = ({ collapsed }: SidebarProps) => {
|
export const Sidebar = ({ collapsed }: SidebarProps) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
|
${
|
||||||
border-r flex flex-col h-full
|
theme === "dark"
|
||||||
${collapsed ? 'w-16' : 'w-64'}
|
? "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} />
|
<SidebarHeader collapsed={collapsed} />
|
||||||
<MainNavigation collapsed={collapsed} />
|
<MainNavigation collapsed={collapsed} />
|
||||||
<SettingsPanel collapsed={collapsed} />
|
<SettingsPanel collapsed={collapsed} />
|
||||||
|
<div className="items-center ml-2 p-2 h-30 border-t ">
|
||||||
|
Version: latest
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MenuItem } from "./MenuItem";
|
import { MenuItem } from "./MenuItem";
|
||||||
import { mainMenuItems } from "./navigationData";
|
import { mainMenuItems } from "./navigationData";
|
||||||
@@ -7,9 +6,15 @@ interface MainNavigationProps {
|
|||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
|
export const MainNavigation: React.FC<MainNavigationProps> = ({
|
||||||
|
collapsed,
|
||||||
|
}) => {
|
||||||
return (
|
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) => (
|
{mainMenuItems.map((item) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -24,4 +29,4 @@ export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) =>
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
@@ -22,7 +21,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
|||||||
translationKey,
|
translationKey,
|
||||||
color,
|
color,
|
||||||
hasNavigation,
|
hasNavigation,
|
||||||
collapsed
|
collapsed,
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
@@ -32,7 +31,7 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
|||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (hasNavigation && path) {
|
if (hasNavigation && path) {
|
||||||
navigate(path, { replace: false });
|
navigate(path, { replace: false });
|
||||||
}
|
}
|
||||||
@@ -42,23 +41,29 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
|||||||
const mainIconSize = "h-6 w-6";
|
const mainIconSize = "h-6 w-6";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
${collapsed ? 'p-3' : 'p-2 pl-3'}
|
${collapsed ? "p-3" : "p-2 pl-3"}
|
||||||
mb-1 rounded-lg
|
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
|
flex items-center
|
||||||
${collapsed ? 'justify-center' : ''}
|
${collapsed ? "justify-center" : ""}
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
`}
|
`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<Icon className={`${mainIconSize} ${color}`} />
|
<Icon className={`${mainIconSize} ${color}`} />
|
||||||
{!collapsed && (
|
{!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)}
|
{t(translationKey)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { Settings, ChevronDown } from "lucide-react";
|
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 { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { settingsMenuItems } from "./navigationData";
|
import { settingsMenuItems } from "./navigationData";
|
||||||
|
|
||||||
@@ -17,19 +20,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
|||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
|
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>(
|
||||||
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
"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
|
// Update active settings item based on URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.pathname === '/settings') {
|
if (location.pathname === "/settings") {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const panel = params.get('panel');
|
const panel = params.get("panel");
|
||||||
if (panel) {
|
if (panel) {
|
||||||
setActiveSettingsItem(panel);
|
setActiveSettingsItem(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [location]);
|
localStorage.setItem("settingsPanelOpen", settingsPanelOpen.toString());
|
||||||
|
}, [location, settingsPanelOpen]);
|
||||||
|
|
||||||
const handleSettingsItemClick = (item: string) => {
|
const handleSettingsItemClick = (item: string) => {
|
||||||
setActiveSettingsItem(item);
|
setActiveSettingsItem(item);
|
||||||
@@ -43,15 +54,27 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getMenuItemClasses = (isActive: boolean) => {
|
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) {
|
if (collapsed) {
|
||||||
const mainIconSize = "h-6 w-6";
|
const mainIconSize = "h-6 w-6";
|
||||||
return (
|
return (
|
||||||
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
|
<div
|
||||||
<div
|
className={`border-t ${
|
||||||
onClick={(e) => handleMenuItemClick('/settings', e)}
|
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"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<Settings className={`${mainIconSize} text-purple-400`} />
|
<Settings className={`${mainIconSize} text-purple-400`} />
|
||||||
@@ -61,33 +84,59 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
|
<div
|
||||||
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
|
className={`flex-1 flex flex-col border-t ${
|
||||||
<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'}`}>
|
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">
|
<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>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Settings className="h-4 w-4 mr-1" />
|
<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>
|
</div>
|
||||||
</CollapsibleTrigger>
|
</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">
|
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="space-y-2 pr-4">
|
<div className="space-y-2 pr-4 cursor-pointer">
|
||||||
{settingsMenuItems.map((item) => (
|
{settingsMenuItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={getMenuItemClasses(activeSettingsItem === item.id)}
|
className={getMenuItemClasses(
|
||||||
|
activeSettingsItem === item.id
|
||||||
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
handleMenuItemClick(`/settings?panel=${item.id}`, e);
|
handleMenuItemClick(`/settings?panel=${item.id}`, e);
|
||||||
handleSettingsItemClick(item.id);
|
handleSettingsItemClick(item.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<item.icon className="h-4 w-4 mr-2" />
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -97,4 +146,4 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
@@ -10,15 +9,17 @@ export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
<div
|
||||||
<div className="h-8 w-8 bg-gray-600 rounded flex items-center justify-center mr-2">
|
className={`p-[15.7px] ${
|
||||||
<img
|
theme === "dark"
|
||||||
src="/favicon_sidebar.ico"
|
? "border-[#1e1e1e] bg-black"
|
||||||
alt="CheckCle"
|
: "border-sidebar-border bg-white"
|
||||||
className="h-6 w-6"
|
} 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>
|
</div>
|
||||||
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
|
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useOperationalPages, useDeleteOperationalPage } from '@/hooks/useOperationalPage';
|
import {
|
||||||
import { CreateOperationalPageDialog } from './CreateOperationalPageDialog';
|
useOperationalPages,
|
||||||
import { EditOperationalPageDialog } from './EditOperationalPageDialog';
|
useDeleteOperationalPage,
|
||||||
import { OperationalPageCard } from './OperationalPageCard';
|
} from "@/hooks/useOperationalPage";
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { CreateOperationalPageDialog } from "./CreateOperationalPageDialog";
|
||||||
import { Button } from '@/components/ui/button';
|
import { EditOperationalPageDialog } from "./EditOperationalPageDialog";
|
||||||
import { OperationalPageRecord } from '@/types/operational.types';
|
import { OperationalPageCard } from "./OperationalPageCard";
|
||||||
import { Activity, Plus, RefreshCw } from 'lucide-react';
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
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 {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -18,17 +21,26 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
export const OperationalPageContent = () => {
|
export const OperationalPageContent = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: pages, isLoading, error, refetch, isRefetching } = useOperationalPages();
|
const {
|
||||||
|
data: pages,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
isRefetching,
|
||||||
|
} = useOperationalPages();
|
||||||
const deleteMutation = useDeleteOperationalPage();
|
const deleteMutation = useDeleteOperationalPage();
|
||||||
|
|
||||||
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(null);
|
const [editingPage, setEditingPage] = useState<OperationalPageRecord | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = 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) => {
|
const handleEdit = (page: OperationalPageRecord) => {
|
||||||
setEditingPage(page);
|
setEditingPage(page);
|
||||||
@@ -37,10 +49,10 @@ export const OperationalPageContent = () => {
|
|||||||
|
|
||||||
const handleView = (page: OperationalPageRecord) => {
|
const handleView = (page: OperationalPageRecord) => {
|
||||||
if (page.custom_domain) {
|
if (page.custom_domain) {
|
||||||
window.open(`https://${page.custom_domain}`, '_blank');
|
window.open(`https://${page.custom_domain}`, "_blank");
|
||||||
} else {
|
} else {
|
||||||
// Navigate to the public status page route using the correct format
|
// 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);
|
setDeleteDialogOpen(false);
|
||||||
setPageToDelete(null);
|
setPageToDelete(null);
|
||||||
} catch (error) {
|
} 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">
|
<div className="mb-4">
|
||||||
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
|
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
|
||||||
</div>
|
</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">
|
<p className="text-muted-foreground mb-4">
|
||||||
There was an error loading your operational pages. Please try again.
|
There was an error loading your operational pages. Please try again.
|
||||||
</p>
|
</p>
|
||||||
@@ -82,16 +96,18 @@ export const OperationalPageContent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="w-full container mx-auto px-4 py-8 ">
|
||||||
{/* Header */}
|
{/* 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>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Manage your public status pages and monitor service health
|
Manage your public status pages and monitor service health
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mt-4 sm:mt-0">
|
<div className="flex items-center gap-2 mt-4 sm:mt-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -99,7 +115,9 @@ export const OperationalPageContent = () => {
|
|||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
disabled={isRefetching}
|
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
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<CreateOperationalPageDialog />
|
<CreateOperationalPageDialog />
|
||||||
@@ -135,9 +153,12 @@ export const OperationalPageContent = () => {
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
|
<Activity className="h-12 w-12 text-muted-foreground mx-auto" />
|
||||||
</div>
|
</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">
|
<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>
|
</p>
|
||||||
<CreateOperationalPageDialog />
|
<CreateOperationalPageDialog />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -167,12 +188,12 @@ export const OperationalPageContent = () => {
|
|||||||
<span className="font-medium">Total Pages:</span> {pages.length}
|
<span className="font-medium">Total Pages:</span> {pages.length}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Public Pages:</span>{' '}
|
<span className="font-medium">Public Pages:</span>{" "}
|
||||||
{pages.filter(p => p.is_public === 'true').length}
|
{pages.filter((p) => p.is_public === "true").length}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Operational:</span>{' '}
|
<span className="font-medium">Operational:</span>{" "}
|
||||||
{pages.filter(p => p.status === 'operational').length}
|
{pages.filter((p) => p.status === "operational").length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,7 +212,9 @@ export const OperationalPageContent = () => {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
|
<AlertDialogTitle>Delete Operational Page</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -201,11 +224,11 @@ export const OperationalPageContent = () => {
|
|||||||
className="bg-red-600 hover:bg-red-700"
|
className="bg-red-600 hover:bg-red-700"
|
||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
>
|
>
|
||||||
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
{deleteMutation.isPending ? "Deleting..." : "Delete"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
|
||||||
import { regionalService } from "@/services/regionalService";
|
import { regionalService } from "@/services/regionalService";
|
||||||
import { RegionalService } from "@/types/regional.types";
|
import { RegionalService } from "@/types/regional.types";
|
||||||
@@ -15,24 +20,29 @@ export const RegionalMonitoringContent = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: regionalServices = [], isLoading, error } = useQuery({
|
const {
|
||||||
queryKey: ['regional-services'],
|
data: regionalServices = [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["regional-services"],
|
||||||
queryFn: regionalService.getRegionalServices,
|
queryFn: regionalService.getRegionalServices,
|
||||||
refetchInterval: 30000, // Refetch every 30 seconds
|
refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAgentAdded = () => {
|
const handleAgentAdded = () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
|
||||||
toast({
|
toast({
|
||||||
title: "Regional Agent Added",
|
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) => {
|
const handleDeleteAgent = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await regionalService.deleteRegionalService(id);
|
await regionalService.deleteRegionalService(id);
|
||||||
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
queryClient.invalidateQueries({ queryKey: ["regional-services"] });
|
||||||
toast({
|
toast({
|
||||||
title: "Agent Removed",
|
title: "Agent Removed",
|
||||||
description: "The regional monitoring agent has been 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;
|
const totalAgents = regionalServices.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 ">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground">
|
||||||
Manage distributed monitoring agents across different regions
|
Manage distributed monitoring agents across different regions
|
||||||
</p>
|
</p>
|
||||||
@@ -85,23 +99,25 @@ export const RegionalMonitoringContent = () => {
|
|||||||
<Wifi className="h-4 w-4 text-green-600" />
|
<Wifi className="h-4 w-4 text-green-600" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
|
<div className="text-2xl font-bold text-green-600">
|
||||||
<p className="text-xs text-muted-foreground">
|
{onlineAgents}
|
||||||
Currently connected
|
</div>
|
||||||
</p>
|
<p className="text-xs text-muted-foreground">Currently connected</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<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" />
|
<WifiOff className="h-4 w-4 text-red-600" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
|
<div className="text-2xl font-bold text-red-600">
|
||||||
<p className="text-xs text-muted-foreground">
|
{totalAgents - onlineAgents}
|
||||||
Disconnected agents
|
</div>
|
||||||
</p>
|
<p className="text-xs text-muted-foreground">Disconnected agents</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +125,7 @@ export const RegionalMonitoringContent = () => {
|
|||||||
{/* Agents List */}
|
{/* Agents List */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-xl font-semibold">Regional Agents</h2>
|
<h2 className="text-xl font-semibold">Regional Agents</h2>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
@@ -133,7 +149,8 @@ export const RegionalMonitoringContent = () => {
|
|||||||
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
|
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
|
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
|
||||||
<p className="text-muted-foreground text-center mb-4">
|
<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>
|
</p>
|
||||||
<Button onClick={() => setAddDialogOpen(true)}>
|
<Button onClick={() => setAddDialogOpen(true)}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{regionalServices.map((agent) => (
|
{regionalServices.map((agent) => (
|
||||||
<RegionalAgentCard
|
<RegionalAgentCard
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
agent={agent}
|
agent={agent}
|
||||||
onDelete={() => handleDeleteAgent(agent.id)}
|
onDelete={() => handleDeleteAgent(agent.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -161,4 +178,4 @@ export const RegionalMonitoringContent = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, CalendarClock, AlertCircle } from "lucide-react";
|
import { Plus, CalendarClock, AlertCircle } from "lucide-react";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
|
import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
|
||||||
import { IncidentManagementTab } from "./IncidentManagementTab";
|
import { IncidentManagementTab } from "./IncidentManagementTab";
|
||||||
import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
|
import { CreateMaintenanceDialog } from "./maintenance/CreateMaintenanceDialog";
|
||||||
import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
|
import { CreateIncidentDialog } from "./incident/CreateIncidentDialog";
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
|
import {
|
||||||
|
initMaintenanceNotifications,
|
||||||
|
stopMaintenanceNotifications,
|
||||||
|
} from "@/services/maintenance/maintenanceNotificationService";
|
||||||
|
|
||||||
export const ScheduleIncidentContent = () => {
|
export const ScheduleIncidentContent = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [activeTab, setActiveTab] = useState("maintenance");
|
const [activeTab, setActiveTab] = useState("maintenance");
|
||||||
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
|
const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] =
|
||||||
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
|
useState(false);
|
||||||
|
const [createIncidentDialogOpen, setCreateIncidentDialogOpen] =
|
||||||
|
useState(false);
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
|
const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
// Initialize maintenance notifications when the component mounts
|
// Initialize maintenance notifications when the component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log("Initializing maintenance notifications");
|
// console.log("Initializing maintenance notifications");
|
||||||
initMaintenanceNotifications();
|
initMaintenanceNotifications();
|
||||||
|
|
||||||
// Clean up when the component unmounts
|
// Clean up when the component unmounts
|
||||||
return () => {
|
return () => {
|
||||||
// console.log("Cleaning up maintenance notifications");
|
// console.log("Cleaning up maintenance notifications");
|
||||||
stopMaintenanceNotifications();
|
stopMaintenanceNotifications();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -43,65 +47,67 @@ export const ScheduleIncidentContent = () => {
|
|||||||
const handleMaintenanceCreated = () => {
|
const handleMaintenanceCreated = () => {
|
||||||
// Refresh data by incrementing the refresh trigger
|
// Refresh data by incrementing the refresh trigger
|
||||||
const newTriggerValue = refreshTrigger + 1;
|
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);
|
setRefreshTrigger(newTriggerValue);
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast
|
||||||
toast({
|
toast({
|
||||||
title: t('success'),
|
title: t("success"),
|
||||||
description: t('maintenanceCreatedSuccess'),
|
description: t("maintenanceCreatedSuccess"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIncidentCreated = () => {
|
const handleIncidentCreated = () => {
|
||||||
// Refresh data by incrementing the refresh trigger
|
// Refresh data by incrementing the refresh trigger
|
||||||
const newTriggerValue = incidentRefreshTrigger + 1;
|
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);
|
setIncidentRefreshTrigger(newTriggerValue);
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast
|
||||||
toast({
|
toast({
|
||||||
title: t('success'),
|
title: t("success"),
|
||||||
description: t('incidentCreatedSuccess'),
|
description: t("incidentCreatedSuccess"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6">
|
<main className="flex-1 flex flex-col overflow-auto bg-background dark:bg-black p-2 ">
|
||||||
<div className="flex flex-col gap-6">
|
<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">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-2xl font-bold text-foreground">
|
<h2 className="text-2xl font-bold text-foreground">
|
||||||
{t('scheduleIncidentManagement')}
|
{t("scheduleIncidentManagement")}
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
className="text-primary-foreground"
|
className="text-primary-foreground"
|
||||||
onClick={handleCreateButtonClick}
|
onClick={handleCreateButtonClick}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
{activeTab === "maintenance" ? t('createMaintenanceWindow') : t('createIncident')}
|
{activeTab === "maintenance"
|
||||||
|
? t("createMaintenanceWindow")
|
||||||
|
: t("createIncident")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue="maintenance"
|
defaultValue="maintenance"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(value) => setActiveTab(value)}
|
onValueChange={(value) => setActiveTab(value)}
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||||
<TabsTrigger value="maintenance">
|
<TabsTrigger value="maintenance">
|
||||||
<CalendarClock className="w-4 h-4 mr-2" />
|
<CalendarClock className="w-4 h-4 mr-2" />
|
||||||
{t('scheduledMaintenance')}
|
{t("scheduledMaintenance")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="incidents">
|
<TabsTrigger value="incidents">
|
||||||
<AlertCircle className="w-4 h-4 mr-2" />
|
<AlertCircle className="w-4 h-4 mr-2" />
|
||||||
{t('incidentManagement')}
|
{t("incidentManagement")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="maintenance" className="space-y-4">
|
<TabsContent value="maintenance" className="space-y-4">
|
||||||
<ScheduledMaintenanceTab refreshTrigger={refreshTrigger} />
|
<ScheduledMaintenanceTab refreshTrigger={refreshTrigger} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="incidents" className="space-y-4">
|
<TabsContent value="incidents" className="space-y-4">
|
||||||
<IncidentManagementTab refreshTrigger={incidentRefreshTrigger} />
|
<IncidentManagementTab refreshTrigger={incidentRefreshTrigger} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -109,14 +115,14 @@ export const ScheduleIncidentContent = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Maintenance creation dialog */}
|
{/* Maintenance creation dialog */}
|
||||||
<CreateMaintenanceDialog
|
<CreateMaintenanceDialog
|
||||||
open={createMaintenanceDialogOpen}
|
open={createMaintenanceDialogOpen}
|
||||||
onOpenChange={setCreateMaintenanceDialogOpen}
|
onOpenChange={setCreateMaintenanceDialogOpen}
|
||||||
onMaintenanceCreated={handleMaintenanceCreated}
|
onMaintenanceCreated={handleMaintenanceCreated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Incident creation dialog */}
|
{/* Incident creation dialog */}
|
||||||
<CreateIncidentDialog
|
<CreateIncidentDialog
|
||||||
open={createIncidentDialogOpen}
|
open={createIncidentDialogOpen}
|
||||||
onOpenChange={setCreateIncidentDialogOpen}
|
onOpenChange={setCreateIncidentDialogOpen}
|
||||||
onIncidentCreated={handleIncidentCreated}
|
onIncidentCreated={handleIncidentCreated}
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import {
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
DropdownMenu,
|
||||||
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
|
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 { Server } from "@/types/server.types";
|
||||||
import { ServerStatusBadge } from "./ServerStatusBadge";
|
import { ServerStatusBadge } from "./ServerStatusBadge";
|
||||||
import { OSTypeIcon } from "./OSTypeIcon";
|
import { OSTypeIcon } from "./OSTypeIcon";
|
||||||
@@ -23,7 +55,11 @@ interface ServerTableProps {
|
|||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
|
export const ServerTable = ({
|
||||||
|
servers,
|
||||||
|
isLoading,
|
||||||
|
onRefresh,
|
||||||
|
}: ServerTableProps) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@@ -34,10 +70,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const filteredServers = servers.filter(server =>
|
const filteredServers = servers.filter(
|
||||||
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(server) =>
|
||||||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleViewDetails = (serverId: string) => {
|
const handleViewDetails = (serverId: string) => {
|
||||||
@@ -51,41 +88,44 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
const handlePauseResume = async (server: Server) => {
|
const handlePauseResume = async (server: Server) => {
|
||||||
const serverId = server.id;
|
const serverId = server.id;
|
||||||
const isPaused = server.status === "paused";
|
const isPaused = server.status === "paused";
|
||||||
|
|
||||||
if (pausingServers.has(serverId)) {
|
if (pausingServers.has(serverId)) {
|
||||||
return; // Already processing this server
|
return; // Already processing this server
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
// Only update the status field, preserving all other server configuration
|
||||||
const updateData = {
|
const updateData = {
|
||||||
status: isPaused ? "up" : "paused",
|
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({
|
toast({
|
||||||
title: isPaused ? "Server resumed" : "Server paused",
|
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
|
// Refresh the server list to show updated status
|
||||||
onRefresh();
|
onRefresh();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('Error updating server status:', error);
|
// console.error('Error updating server status:', error);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error",
|
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 {
|
} finally {
|
||||||
setPausingServers(prev => {
|
setPausingServers((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
newSet.delete(serverId);
|
newSet.delete(serverId);
|
||||||
return newSet;
|
return newSet;
|
||||||
@@ -108,24 +148,23 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
|
||||||
// Delete the server from the database
|
// Delete the server from the database
|
||||||
await pb.collection('servers').delete(selectedServer.id);
|
await pb.collection("servers").delete(selectedServer.id);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Server deleted",
|
title: "Server deleted",
|
||||||
description: `${selectedServer.name} has been deleted successfully.`,
|
description: `${selectedServer.name} has been deleted successfully.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh the server list
|
// Refresh the server list
|
||||||
onRefresh();
|
onRefresh();
|
||||||
|
|
||||||
// Close the dialog
|
// Close the dialog
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setSelectedServer(null);
|
setSelectedServer(null);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('Error deleting server:', error);
|
// console.error('Error deleting server:', error);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -136,65 +175,65 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomProgressBar = ({
|
const CustomProgressBar = ({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
subtitle,
|
subtitle,
|
||||||
type
|
type,
|
||||||
}: {
|
}: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
type: 'cpu' | 'memory' | 'disk'
|
type: "cpu" | "memory" | "disk";
|
||||||
}) => {
|
}) => {
|
||||||
const getGradientColors = (type: string, value: number) => {
|
const getGradientColors = (type: string, value: number) => {
|
||||||
if (type === 'cpu') {
|
if (type === "cpu") {
|
||||||
if (value > 90) return 'from-red-500 to-red-600';
|
if (value > 90) return "from-red-500 to-red-600";
|
||||||
if (value > 75) return 'from-orange-500 to-orange-600';
|
if (value > 75) return "from-orange-500 to-orange-600";
|
||||||
if (value > 60) return 'from-yellow-500 to-yellow-600';
|
if (value > 60) return "from-yellow-500 to-yellow-600";
|
||||||
return 'from-green-500 to-green-600';
|
return "from-green-500 to-green-600";
|
||||||
}
|
}
|
||||||
if (type === 'memory') {
|
if (type === "memory") {
|
||||||
if (value > 90) return 'from-red-500 to-red-600';
|
if (value > 90) return "from-red-500 to-red-600";
|
||||||
if (value > 75) return 'from-yellow-500 to-yellow-600';
|
if (value > 75) return "from-yellow-500 to-yellow-600";
|
||||||
return 'from-blue-500 to-blue-600';
|
return "from-blue-500 to-blue-600";
|
||||||
}
|
}
|
||||||
if (type === 'disk') {
|
if (type === "disk") {
|
||||||
if (value > 95) return 'from-red-500 to-red-600';
|
if (value > 95) return "from-red-500 to-red-600";
|
||||||
if (value > 85) return 'from-yellow-500 to-yellow-600';
|
if (value > 85) return "from-yellow-500 to-yellow-600";
|
||||||
return 'from-orange-500 to-orange-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) => {
|
const getTextColor = (value: number) => {
|
||||||
if (value > 90) return 'text-red-600 dark:text-red-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 > 75) return "text-orange-600 dark:text-orange-400";
|
||||||
if (value > 60) return 'text-yellow-600 dark:text-yellow-400';
|
if (value > 60) return "text-yellow-600 dark:text-yellow-400";
|
||||||
return 'text-green-600 dark:text-green-400';
|
return "text-green-600 dark:text-green-400";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex justify-between items-center">
|
||||||
<span className={`text-sm font-semibold ${getTextColor(value)}`}>
|
<span className={`text-sm font-semibold ${getTextColor(value)}`}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">{subtitle} </span>
|
||||||
{subtitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner">
|
<div className="w-full h-3 bg-muted/30 rounded-full overflow-hidden shadow-inner">
|
||||||
<div
|
<div
|
||||||
className={`h-full bg-gradient-to-r ${getGradientColors(type, value)} rounded-full transition-all duration-700 ease-out relative overflow-hidden`}
|
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)}%` }}
|
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-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 className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -202,7 +241,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card className="flex-1 flex flex-col">
|
<Card className="flex-1 flex flex-col ">
|
||||||
<CardHeader className="flex-shrink-0">
|
<CardHeader className="flex-shrink-0">
|
||||||
<CardTitle>Servers</CardTitle>
|
<CardTitle>Servers</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -244,33 +283,113 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
<p className="text-muted-foreground">No servers found</p>
|
<p className="text-muted-foreground">No servers found</p>
|
||||||
</div>
|
</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>
|
<Table>
|
||||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
<TableHeader
|
||||||
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
|
className={`${
|
||||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead>
|
theme === "dark" ? "bg-gray-800" : "bg-gray-50"
|
||||||
<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>
|
<TableRow
|
||||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead>
|
className={`${
|
||||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead>
|
theme === "dark"
|
||||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead>
|
? "border-gray-700 hover:bg-gray-800"
|
||||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead>
|
: "border-gray-200 hover:bg-gray-100"
|
||||||
<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>
|
>
|
||||||
|
<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>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredServers.map((server) => {
|
{filteredServers.map((server) => {
|
||||||
const cpuUsage = server.cpu_usage || 0;
|
const cpuUsage = server.cpu_usage || 0;
|
||||||
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
|
const memoryUsage =
|
||||||
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
|
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 isPaused = server.status === "paused";
|
||||||
const isProcessing = pausingServers.has(server.id);
|
const isProcessing = pausingServers.has(server.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={server.id}
|
key={server.id}
|
||||||
className="hover:bg-muted/50 cursor-pointer"
|
className="hover:bg-muted/50 cursor-pointer"
|
||||||
onClick={() => handleViewDetails(server.id)}
|
onClick={() => handleViewDetails(server.id)}
|
||||||
>
|
>
|
||||||
@@ -285,7 +404,10 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<OSTypeIcon osType={server.os_type} />
|
<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}
|
{server.os_type}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,7 +429,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
<CustomProgressBar
|
<CustomProgressBar
|
||||||
value={memoryUsage}
|
value={memoryUsage}
|
||||||
label={`${memoryUsage.toFixed(1)}%`}
|
label={`${memoryUsage.toFixed(1)}%`}
|
||||||
subtitle={serverService.formatBytes(server.ram_total)}
|
subtitle={serverService.formatBytes(
|
||||||
|
server.ram_total
|
||||||
|
)}
|
||||||
type="memory"
|
type="memory"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -315,12 +439,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
<CustomProgressBar
|
<CustomProgressBar
|
||||||
value={diskUsage}
|
value={diskUsage}
|
||||||
label={`${diskUsage.toFixed(1)}%`}
|
label={`${diskUsage.toFixed(1)}%`}
|
||||||
subtitle={serverService.formatBytes(server.disk_total)}
|
subtitle={serverService.formatBytes(
|
||||||
|
server.disk_total
|
||||||
|
)}
|
||||||
type="disk"
|
type="disk"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm truncate" title={server.uptime}>
|
<div
|
||||||
|
className="text-sm truncate"
|
||||||
|
title={server.uptime}
|
||||||
|
>
|
||||||
{server.uptime}
|
{server.uptime}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -329,10 +458,17 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
{new Date(server.last_checked).toLocaleString()}
|
{new Date(server.last_checked).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
<TableCell
|
||||||
|
className="text-right"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
<span className="sr-only">Open menu</span>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
@@ -341,20 +477,36 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-[200px]">
|
<DropdownMenuContent
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
|
align="end"
|
||||||
|
className="w-[200px]"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleViewDetails(server.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Eye className="mr-2 h-4 w-4" />
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
View Server Detail
|
View Server Detail
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{server.docker === 'true' && (
|
{server.docker === "true" && (
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleViewContainers(server.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Activity className="mr-2 h-4 w-4" />
|
<Activity className="mr-2 h-4 w-4" />
|
||||||
Container Monitoring
|
Container Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePauseResume(server);
|
||||||
|
}}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
{isPaused ? (
|
{isPaused ? (
|
||||||
@@ -370,12 +522,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEdit(server);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit Server
|
Edit Server
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(server);
|
||||||
|
}}
|
||||||
className="text-red-600 focus:text-red-600"
|
className="text-red-600 focus:text-red-600"
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
@@ -406,19 +566,19 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
|||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
|
Are you sure you want to delete this server?
|
||||||
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<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">
|
<span className="font-semibold text-foreground">
|
||||||
{selectedServer?.name}
|
{selectedServer?.name}
|
||||||
</span>{' '}
|
</span>{" "}
|
||||||
and all of its monitoring data.
|
and all of its monitoring data.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={isDeleting}>
|
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={confirmDelete}
|
onClick={confirmDelete}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
|
|||||||
@@ -25,19 +25,21 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
|
|||||||
return start;
|
return start;
|
||||||
}, [endDate, interval]);
|
}, [endDate, interval]);
|
||||||
|
|
||||||
// Fetch uptime data with very aggressive caching to reduce API calls
|
// Fetch uptime data with immediate display and background updates
|
||||||
const { data: uptimeData = [] } = useQuery({
|
const { data: uptimeData = [] } = useQuery({
|
||||||
queryKey: ['uptime-bar', serviceId, serviceType],
|
queryKey: ['uptime-bar', serviceId, serviceType],
|
||||||
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
|
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
|
||||||
enabled: !!serviceId,
|
enabled: !!serviceId,
|
||||||
staleTime: 30000, // Data is fresh for 30 seconds
|
staleTime: 300000, // Data stays fresh for 5 minutes - display immediately from cache
|
||||||
gcTime: 600000, // Keep in cache for 10 minutes
|
gcTime: 900000, // Keep in cache for 15 minutes
|
||||||
refetchInterval: 60000, // 1 minute polling
|
refetchInterval: 300000, // Reduced to 5 minute polling
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: 3,
|
retry: 1, // Reduce retries for faster failure
|
||||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
retryDelay: 1000, // Faster retry delay
|
||||||
|
refetchIntervalInBackground: true, // Allow background updates
|
||||||
|
placeholderData: (previousData) => previousData, // Keep showing previous data while refetching
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize the uptime calculation to prevent unnecessary recalculations
|
// Memoize the uptime calculation to prevent unnecessary recalculations
|
||||||
|
|||||||
@@ -2,13 +2,24 @@ import React, { useState } from "react";
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, RefreshCw } from "lucide-react";
|
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 { toast } from "sonner";
|
||||||
|
|
||||||
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
|
import { SSLCertificateStatusCards } from "./SSLCertificateStatusCards";
|
||||||
import { SSLCertificatesTable } from "./SSLCertificatesTable";
|
import { SSLCertificatesTable } from "./SSLCertificatesTable";
|
||||||
import { LoadingState } from "@/components/services/LoadingState";
|
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 { AddSSLCertificateForm } from "./AddSSLCertificateForm";
|
||||||
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
|
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
|
||||||
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
|
import type { AddSSLCertificateDto, SSLCertificate } from "@/types/ssl.types";
|
||||||
@@ -21,18 +32,23 @@ export const SSLDomainContent = () => {
|
|||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
const [refreshingId, setRefreshingId] = useState<string | null>(null);
|
const [refreshingId, setRefreshingId] = useState<string | null>(null);
|
||||||
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
|
const [isRefreshingAll, setIsRefreshingAll] = useState(false);
|
||||||
const [selectedCertificate, setSelectedCertificate] = useState<SSLCertificate | null>(null);
|
const [selectedCertificate, setSelectedCertificate] =
|
||||||
|
useState<SSLCertificate | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Fetch SSL certificates with explicit error handling
|
// Fetch SSL certificates with explicit error handling
|
||||||
const { data: certificates = [], isLoading, error } = useQuery({
|
const {
|
||||||
queryKey: ['ssl-certificates'],
|
data: certificates = [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["ssl-certificates"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await fetchSSLCertificates();
|
const result = await fetchSSLCertificates();
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('failedToLoadCertificates'));
|
toast.error(t("failedToLoadCertificates"));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -44,101 +60,114 @@ export const SSLDomainContent = () => {
|
|||||||
const addMutation = useMutation({
|
const addMutation = useMutation({
|
||||||
mutationFn: addSSLCertificate,
|
mutationFn: addSSLCertificate,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||||
setIsAddDialogOpen(false);
|
setIsAddDialogOpen(false);
|
||||||
toast.success(t('sslCertificateAdded'));
|
toast.success(t("sslCertificateAdded"));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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
|
// Edit certificate mutation - Updated to include notification_id and template_id
|
||||||
const editMutation = useMutation({
|
const editMutation = useMutation({
|
||||||
mutationFn: async (certificate: SSLCertificate) => {
|
mutationFn: async (certificate: SSLCertificate) => {
|
||||||
|
|
||||||
// Create the update data object with new fields
|
// Create the update data object with new fields
|
||||||
const updateData = {
|
const updateData = {
|
||||||
warning_threshold: Number(certificate.warning_threshold),
|
warning_threshold: Number(certificate.warning_threshold),
|
||||||
expiry_threshold: Number(certificate.expiry_threshold),
|
expiry_threshold: Number(certificate.expiry_threshold),
|
||||||
notification_channel: certificate.notification_channel,
|
notification_channel: certificate.notification_channel,
|
||||||
notification_id: certificate.notification_id || '', // Multi notification channels
|
notification_id: certificate.notification_id || "", // Multi notification channels
|
||||||
template_id: certificate.template_id || '', // Alert template ID
|
template_id: certificate.template_id || "", // Alert template ID
|
||||||
check_interval: certificate.check_interval,
|
check_interval: certificate.check_interval,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Update certificate in the database using PocketBase directly
|
// 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
|
// 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
|
// This will also check if notification needs to be sent based on updated thresholds
|
||||||
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
|
const refreshedCert = await checkAndUpdateCertificate(certificate.id);
|
||||||
|
|
||||||
return refreshedCert;
|
return refreshedCert;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||||
setIsEditDialogOpen(false);
|
setIsEditDialogOpen(false);
|
||||||
setSelectedCertificate(null);
|
setSelectedCertificate(null);
|
||||||
toast.success(t('sslCertificateUpdated'));
|
toast.success(t("sslCertificateUpdated"));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
|
toast.error(
|
||||||
}
|
error instanceof Error ? error.message : t("failedToUpdateCertificate")
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete certificate mutation
|
// Delete certificate mutation
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: deleteSSLCertificate,
|
mutationFn: deleteSSLCertificate,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||||
toast.success(t('sslCertificateDeleted'));
|
toast.success(t("sslCertificateDeleted"));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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
|
// Refresh certificate mutation - Updated to remove individual toast notifications
|
||||||
const refreshMutation = useMutation({
|
const refreshMutation = useMutation({
|
||||||
mutationFn: checkAndUpdateCertificate,
|
mutationFn: checkAndUpdateCertificate,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||||
setRefreshingId(null);
|
setRefreshingId(null);
|
||||||
// Removed individual success toast notification
|
// Removed individual success toast notification
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setRefreshingId(null);
|
setRefreshingId(null);
|
||||||
|
|
||||||
// Still refresh the data to show any partial information that was saved
|
// 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
|
// Removed individual error toast notification
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh all certificates mutation
|
// Refresh all certificates mutation
|
||||||
const refreshAllMutation = useMutation({
|
const refreshAllMutation = useMutation({
|
||||||
mutationFn: refreshAllCertificates,
|
mutationFn: refreshAllCertificates,
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||||
setIsRefreshingAll(false);
|
setIsRefreshingAll(false);
|
||||||
|
|
||||||
if (result.failed === 0) {
|
if (result.failed === 0) {
|
||||||
toast.success(t('allCertificatesRefreshed').replace('{count}', result.success.toString()));
|
toast.success(
|
||||||
|
t("allCertificatesRefreshed").replace(
|
||||||
|
"{count}",
|
||||||
|
result.success.toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.info(t('someCertificatesFailed')
|
toast.info(
|
||||||
.replace('{success}', result.success.toString())
|
t("someCertificatesFailed")
|
||||||
.replace('{failed}', result.failed.toString()));
|
.replace("{success}", result.success.toString())
|
||||||
|
.replace("{failed}", result.failed.toString())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(t('failedToCheckCertificate'));
|
toast.error(t("failedToCheckCertificate"));
|
||||||
setIsRefreshingAll(false);
|
setIsRefreshingAll(false);
|
||||||
|
|
||||||
// Still refresh the data to show any partial information
|
// Still refresh the data to show any partial information
|
||||||
queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] });
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] });
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
|
const handleAddCertificate = async (data: AddSSLCertificateDto) => {
|
||||||
@@ -166,12 +195,14 @@ export const SSLDomainContent = () => {
|
|||||||
|
|
||||||
const handleRefreshAll = async () => {
|
const handleRefreshAll = async () => {
|
||||||
if (certificates.length === 0) {
|
if (certificates.length === 0) {
|
||||||
toast.info(t('noCertificatesToRefresh'));
|
toast.info(t("noCertificatesToRefresh"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRefreshingAll(true);
|
setIsRefreshingAll(true);
|
||||||
toast.info(t('startingRefreshAll').replace('{count}', certificates.length.toString()));
|
toast.info(
|
||||||
|
t("startingRefreshAll").replace("{count}", certificates.length.toString())
|
||||||
|
);
|
||||||
refreshAllMutation.mutate();
|
refreshAllMutation.mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,48 +213,60 @@ export const SSLDomainContent = () => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
|
||||||
<p>{t('failedToLoadCertificates')}</p>
|
<p>{t("failedToLoadCertificates")}</p>
|
||||||
<Button onClick={() => queryClient.invalidateQueries({ queryKey: ['ssl-certificates'] })}>
|
<Button
|
||||||
{t('check')}
|
onClick={() =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ssl-certificates"] })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("check")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
|
<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">
|
<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 className="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-foreground">{t('sslDomainManagement')}</h2>
|
<h2 className="text-2xl font-bold text-foreground">
|
||||||
<p className="text-sm text-muted-foreground mt-1">{t('monitorSSLCertificates')}</p>
|
{t("sslDomainManagement")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{t("monitorSSLCertificates")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleRefreshAll}
|
onClick={handleRefreshAll}
|
||||||
disabled={isRefreshingAll || refreshingId !== null}
|
disabled={isRefreshingAll || refreshingId !== null}
|
||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshingAll ? 'animate-spin' : ''}`} />
|
<RefreshCw
|
||||||
{t('refreshAll')}
|
className={`w-4 h-4 mr-2 ${
|
||||||
|
isRefreshingAll ? "animate-spin" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{t("refreshAll")}
|
||||||
{isRefreshingAll && (
|
{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 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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="text-primary-foreground"
|
className="text-primary-foreground"
|
||||||
onClick={() => setIsAddDialogOpen(true)}
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" /> {t('addDomain')}
|
<Plus className="w-4 h-4 mr-2" /> {t("addDomain")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SSLCertificateStatusCards certificates={certificates} />
|
<SSLCertificateStatusCards certificates={certificates} />
|
||||||
|
|
||||||
<div className="mt-6 flex-1 flex flex-col pb-6">
|
<div className="mt-6 flex-1 flex flex-col pb-6">
|
||||||
<SSLCertificatesTable />
|
<SSLCertificatesTable />
|
||||||
</div>
|
</div>
|
||||||
@@ -232,26 +275,29 @@ export const SSLDomainContent = () => {
|
|||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('addSSLCertificate')}</DialogTitle>
|
<DialogTitle>{t("addSSLCertificate")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<AddSSLCertificateForm
|
<AddSSLCertificateForm
|
||||||
onSubmit={handleAddCertificate}
|
onSubmit={handleAddCertificate}
|
||||||
onCancel={() => setIsAddDialogOpen(false)}
|
onCancel={() => setIsAddDialogOpen(false)}
|
||||||
isPending={addMutation.isPending}
|
isPending={addMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{selectedCertificate && (
|
{selectedCertificate && (
|
||||||
<Dialog open={isEditDialogOpen} onOpenChange={(open) => {
|
<Dialog
|
||||||
setIsEditDialogOpen(open);
|
open={isEditDialogOpen}
|
||||||
if (!open) setSelectedCertificate(null);
|
onOpenChange={(open) => {
|
||||||
}}>
|
setIsEditDialogOpen(open);
|
||||||
|
if (!open) setSelectedCertificate(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('editSSLCertificate')}</DialogTitle>
|
<DialogTitle>{t("editSSLCertificate")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<EditSSLCertificateForm
|
<EditSSLCertificateForm
|
||||||
certificate={selectedCertificate}
|
certificate={selectedCertificate}
|
||||||
onSubmit={handleUpdateCertificate}
|
onSubmit={handleUpdateCertificate}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
@@ -265,4 +311,4 @@ export const SSLDomainContent = () => {
|
|||||||
)}
|
)}
|
||||||
</main>
|
</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, useEffect } from "react";
|
||||||
import React, { createContext, useContext, useState } from 'react';
|
type SideBar = boolean;
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: SideBar;
|
||||||
setSidebarCollapsed: (collapsed: boolean) => void;
|
setSidebarCollapsed: React.Dispatch<React.SetStateAction<SideBar>>;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||||
|
interface SidebarProviderProps {
|
||||||
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
children: React.ReactNode;
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
}
|
||||||
|
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 = () => {
|
const toggleSidebar = () => {
|
||||||
setSidebarCollapsed(prev => !prev);
|
setSidebarCollapsed((prev) => {
|
||||||
|
const newState = !prev;
|
||||||
|
localStorage.setItem("sidebarCollapsed", newState.toString());
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
setSidebarCollapsed,
|
setSidebarCollapsed,
|
||||||
toggleSidebar
|
toggleSidebar,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={value}>
|
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
|
||||||
{children}
|
|
||||||
</SidebarContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A custom hook to consume the sidebar context.
|
||||||
export const useSidebar = () => {
|
export const useSidebar = () => {
|
||||||
const context = useContext(SidebarContext);
|
const context = useContext(SidebarContext);
|
||||||
if (context === undefined) {
|
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;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
@@ -21,61 +20,69 @@ const InstanceMonitoring = () => {
|
|||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [stats, setStats] = useState<ServerStats>({
|
const [stats, setStats] = useState<ServerStats>({
|
||||||
total: 0,
|
total: 0,
|
||||||
online: 0,
|
online: 0,
|
||||||
offline: 0,
|
offline: 0,
|
||||||
warning: 0
|
warning: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
|
|
||||||
const { data: servers = [], isLoading, error, refetch } = useQuery({
|
const {
|
||||||
queryKey: ['servers'],
|
data: servers = [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["servers"],
|
||||||
queryFn: serverService.getServers,
|
queryFn: serverService.getServers,
|
||||||
refetchInterval: 30000 // Refetch every 30 seconds
|
refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (servers.length > 0) {
|
if (servers.length > 0) {
|
||||||
serverService.getServerStats(servers).then(setStats);
|
serverService.getServerStats(servers).then(setStats);
|
||||||
}
|
}
|
||||||
}, [servers]);
|
}, [servers]);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
refetch();
|
refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authService.logout();
|
authService.logout();
|
||||||
navigate('/login');
|
navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAgentAdded = () => {
|
const handleAgentAdded = () => {
|
||||||
refetch();
|
refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
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} />
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
<div className="flex flex-col flex-1 min-w-0">
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
<Header
|
<Header
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
toggleSidebar={toggleSidebar}
|
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">
|
<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">
|
<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>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base"
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
@@ -86,22 +93,22 @@ const InstanceMonitoring = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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} />
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
<div className="flex flex-col flex-1 min-w-0">
|
<div className="flex flex-col flex-1 min-w-0 ">
|
||||||
<Header
|
<Header
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
/>
|
/>
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto dark:bg-black p-2 shadow">
|
||||||
<div className="p-4 sm:p-6 lg:p-8 space-y-6">
|
<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 */}
|
{/* Header Section */}
|
||||||
<div>
|
<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">
|
<div className="min-w-0 flex-1">
|
||||||
<h1 className="text-2xl lg:text-2xl font-bold text-foreground">
|
<h1 className="text-2xl lg:text-2xl font-bold text-foreground">
|
||||||
Instance Monitoring
|
Instance Monitoring
|
||||||
@@ -110,7 +117,10 @@ const InstanceMonitoring = () => {
|
|||||||
Monitor and manage your server instances in real-time
|
Monitor and manage your server instances in real-time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Server Agent
|
Add Server Agent
|
||||||
</Button>
|
</Button>
|
||||||
@@ -118,13 +128,17 @@ const InstanceMonitoring = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards Section */}
|
{/* Stats Cards Section */}
|
||||||
<div>
|
<div className="p-2 ">
|
||||||
<ServerStatsCards stats={stats} />
|
<ServerStatsCards stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server Table Section */}
|
{/* Server Table Section */}
|
||||||
<div>
|
<div className="p-3">
|
||||||
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
|
<ServerTable
|
||||||
|
servers={servers}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -139,4 +153,4 @@ const InstanceMonitoring = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InstanceMonitoring;
|
export default InstanceMonitoring;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { authService } from "@/services/authService";
|
import { authService } from "@/services/authService";
|
||||||
@@ -7,32 +6,41 @@ import { Header } from "@/components/dashboard/Header";
|
|||||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||||
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
|
import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
|
||||||
import { LoadingState } from "@/components/services/LoadingState";
|
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 { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { useSidebar } from "@/contexts/SidebarContext";
|
import { useSidebar } from "@/contexts/SidebarContext";
|
||||||
|
|
||||||
const SslDomain = () => {
|
const SslDomain = () => {
|
||||||
// Get language context for translations
|
// Get language context for translations
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Use shared sidebar state
|
// Use shared sidebar state
|
||||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
// Get current user
|
// Get current user
|
||||||
const currentUser = authService.getCurrentUser();
|
const currentUser = authService.getCurrentUser();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Fetch SSL certificates with error handling and debugging
|
// Fetch SSL certificates with error handling and debugging
|
||||||
const { data: certificates = [], isLoading, error, refetch } = useQuery({
|
const {
|
||||||
queryKey: ['ssl-certificates'],
|
data: certificates = [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["ssl-certificates"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// console.log("Fetching SSL certificates from SslDomain page...");
|
// console.log("Fetching SSL certificates from SslDomain page...");
|
||||||
try {
|
try {
|
||||||
const result = await fetchSSLCertificates();
|
const result = await fetchSSLCertificates();
|
||||||
// console.log("SSL certificates fetch successful, count:", result.length);
|
// console.log("SSL certificates fetch successful, count:", result.length);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.error("Error fetching SSL certificates from page:", err);
|
// console.error("Error fetching SSL certificates from page:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -40,23 +48,23 @@ const SslDomain = () => {
|
|||||||
refetchInterval: 300000, // Refresh every 5 minutes
|
refetchInterval: 300000, // Refresh every 5 minutes
|
||||||
retry: 3, // Retry failed requests 3 times
|
retry: 3, // Retry failed requests 3 times
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check all SSL certificates once per day
|
// Check all SSL certificates once per day
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkCertificates = async () => {
|
const checkCertificates = async () => {
|
||||||
// Check if we should run daily check
|
// Check if we should run daily check
|
||||||
if (shouldRunDailyCheck()) {
|
if (shouldRunDailyCheck()) {
|
||||||
// console.log("Running daily SSL certificate check...");
|
// console.log("Running daily SSL certificate check...");
|
||||||
await checkAllCertificatesAndNotify();
|
await checkAllCertificatesAndNotify();
|
||||||
// Refresh certificate list after daily check
|
// Refresh certificate list after daily check
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run check when component mounts
|
// Run check when component mounts
|
||||||
checkCertificates();
|
checkCertificates();
|
||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
// Handle logout
|
// Handle logout
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authService.logout();
|
authService.logout();
|
||||||
@@ -69,11 +77,11 @@ const SslDomain = () => {
|
|||||||
<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} />
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1">
|
||||||
<Header
|
<Header
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
/>
|
/>
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
</div>
|
</div>
|
||||||
@@ -86,23 +94,25 @@ const SslDomain = () => {
|
|||||||
return (
|
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} />
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1 ">
|
||||||
<Header
|
<Header
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center h-full p-6">
|
<div className="flex flex-col items-center justify-center h-full p-6 ">
|
||||||
<h2 className="text-xl font-bold mb-2">{t('failedToLoadCertificates')}</h2>
|
<h2 className="text-xl font-bold mb-2">
|
||||||
|
{t("failedToLoadCertificates")}
|
||||||
|
</h2>
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
{error instanceof Error ? error.message : t('unknown')}
|
{error instanceof Error ? error.message : t("unknown")}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
>
|
>
|
||||||
{t('check')}
|
{t("check")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,12 +124,12 @@ const SslDomain = () => {
|
|||||||
return (
|
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} />
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1 ">
|
||||||
<Header
|
<Header
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
/>
|
/>
|
||||||
<SSLDomainContent />
|
<SSLDomainContent />
|
||||||
</div>
|
</div>
|
||||||
@@ -127,4 +137,4 @@ const SslDomain = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SslDomain;
|
export default SslDomain;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ The roadmap is divided into the following stages:
|
|||||||
- [ ] ✅ Add ntfy push notifications
|
- [ ] ✅ Add ntfy push notifications
|
||||||
|
|
||||||
### 🚀 v1.6.0 – Feature Enhancements _(Target: Q3 2025)_
|
### 🚀 v1.6.0 – Feature Enhancements _(Target: Q3 2025)_
|
||||||
**Status:** 🔧 In Progress
|
**Status:** ✅ Completed
|
||||||
#### Tentative Features:
|
#### Tentative Features:
|
||||||
- [ ] ✅ Allow user to update the schema directly from the dashboard
|
- [ ] ✅ Allow user to update the schema directly from the dashboard
|
||||||
- [ ] ✅ Add Korean translations and update types. By @taking [#143](https://github.com/operacle/checkcle/pull/143)
|
- [ ] ✅ Add Korean translations and update types. By @taking [#143](https://github.com/operacle/checkcle/pull/143)
|
||||||
@@ -89,7 +89,14 @@ The roadmap is divided into the following stages:
|
|||||||
- [ ] ✅ fix(ssl): Ensure edit form saves notification_id and template_id in DB.
|
- [ ] ✅ fix(ssl): Ensure edit form saves notification_id and template_id in DB.
|
||||||
- [ ] ✅ Add Notifiarr notifications
|
- [ ] ✅ Add Notifiarr notifications
|
||||||
- [ ] ✅ Add Gotify notifications
|
- [ ] ✅ Add Gotify notifications
|
||||||
- [ ] 🔧 Enhance the Data retention feature
|
- [ ] ✅ Enhance the Data retention feature
|
||||||
|
|
||||||
|
|
||||||
|
### 🚀 v1.7.0 – Feature Enhancements _(Target: Q3 2025)_
|
||||||
|
**Status:** 🔧 In Progress
|
||||||
|
#### Tentative Features:
|
||||||
|
- [ ] 🔧 Fix for the response time dashboard filter (date/time) in the uptime detail page
|
||||||
|
- [ ] 🔧 Fix the server historical loading data
|
||||||
- [ ] 🔧 Improve docker agent for server monitoring
|
- [ ] 🔧 Improve docker agent for server monitoring
|
||||||
- [ ] 🔧 Improve the Server Agent on Unraid
|
- [ ] 🔧 Improve the Server Agent on Unraid
|
||||||
- [ ] 🔧 Server Windows Agent
|
- [ ] 🔧 Server Windows Agent
|
||||||
|
|||||||
Reference in New Issue
Block a user