switch to typescript

This commit is contained in:
2025-07-15 22:49:24 +02:00
parent 97554baee9
commit f4e7328494
55 changed files with 2007 additions and 8708 deletions
+40 -6
View File
@@ -1,7 +1,41 @@
# Generated by Cargo
# will have compiled files and executables
/target
.DS_Store
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# These are backup files generated by rustfmt
**/*.rs.bk
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
Generated
-6772
View File
File diff suppressed because it is too large Load Diff
-36
View File
@@ -1,36 +0,0 @@
[package]
name = "sentryx"
version = "0.1.0"
authors = ["Leo dev <[email protected]>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { version = "0.6.0", features = ["router", "fullstack"] }
js-sys = "0.3.77"
lucide-dioxus = { version = "2.15.0", features = ["all-icons"] }
wasm-bindgen = "0.2.100"
web-sys = "0.3.77"
serde = { version = "1.0.61", features = ["derive"]}
mongodb = {version = "3.2.4", optional = true}
features = "0.10.0"
futures-util = "0.3.31"
regex = "1.11.1"
[features]
default = ["web"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
server = ["dioxus/server", "dep:mongodb"]
[profile.wasm-dev]
inherits = "dev"
opt-level = 1
[profile.server-dev]
inherits = "dev"
[profile.android-dev]
inherits = "dev"
-21
View File
@@ -1,21 +0,0 @@
[application]
[web.app]
# HTML title tag content
title = "Sentryx"
# include `assets` in web platform
[web.resource]
# Additional CSS style files
style = []
# Additional JavaScript files
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
+1 -2
View File
@@ -38,7 +38,6 @@
</div>
## 📚 Table of Contents
- [About The Project](#about-the-project)
@@ -47,6 +46,7 @@
---
## About The Project
**Sentryx** is a secure control panel for managing multiple servers. Designed with modern technologies like Next.js and React, it aims to provide a fast, clean, and scalable user interface for server operators and sysadmins.
### Built With
@@ -61,7 +61,6 @@
Not yet :/
[contributors-shield]: https://img.shields.io/github/contributors/orus-dev/sentryx.svg?style=flat-square&color=blue&label=contributors
[contributors-url]: https://github.com/orus-dev/sentryx/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/orus-dev/sentryx.svg?style=flat-square&color=blue
-1
View File
@@ -1 +0,0 @@
<svg class="border shadow-md dark:border-slate-700" viewBox="0 0 1382.968888888889 777.92" style="width: 1382.97px; height: 777.92px;" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none"><rect class="bg" id="bg" x="0" y="0" width="1382.968888888889" height="777.92" fill="#000000"/><g transform="rotate(45 691.4844444444445 388.96)"><path d="M -691.48 607.92 S -355.74 500.92&#xA; 0.00 607.92 204.48 497.92&#xA; 691.48 607.92 1027.23 592.92&#xA; 1382.97 607.92 1718.71 547.92&#xA; 2074.45 607.92 h 110 V 1377.92 H -691.48 Z" fill="#e92560"/><path d="M -691.48 195.00 S -355.74 107.50&#xA; 0.00 195.00 69.48 107.50&#xA; 691.48 195.00 1027.23 40.00&#xA; 1382.97 195.00 1718.71 107.50&#xA; 2074.45 195.00 h 110 V -600 H -691.48 Z" fill="#681a92"/></g></svg>

Before

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000" class="icon icon-tabler icons-tabler-filled icon-tabler-dashboard"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 2.954a10 10 0 0 1 6.222 17.829a1 1 0 0 1 -.622 .217h-11.2a1 1 0 0 1 -.622 -.217a10 10 0 0 1 6.222 -17.829m4.207 5.839a1 1 0 0 0 -1.414 0l-2.276 2.274a2.003 2.003 0 0 0 -2.514 1.815l-.003 .118a2 2 0 1 0 3.933 -.517l2.274 -2.276a1 1 0 0 0 0 -1.414" /></svg>

Before

Width:  |  Height:  |  Size: 487 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dashboard"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 13m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M13.45 11.55l2.05 -2.05" /><path d="M6.4 20a9 9 0 1 1 11.2 0z" /></svg>

Before

Width:  |  Height:  |  Size: 435 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000" class="icon icon-tabler icons-tabler-filled icon-tabler-home"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12.707 2.293l9 9c.63 .63 .184 1.707 -.707 1.707h-1v6a3 3 0 0 1 -3 3h-1v-7a3 3 0 0 0 -2.824 -2.995l-.176 -.005h-2a3 3 0 0 0 -3 3v7h-1a3 3 0 0 1 -3 -3v-6h-1c-.89 0 -1.337 -1.077 -.707 -1.707l9 -9a1 1 0 0 1 1.414 0m.293 11.707a1 1 0 0 1 1 1v7h-4v-7a1 1 0 0 1 .883 -.993l.117 -.007z" /></svg>

Before

Width:  |  Height:  |  Size: 512 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-home"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l-2 0l9 -9l9 9l-2 0" /><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" /><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" /></svg>

Before

Width:  |  Height:  |  Size: 449 B

-331
View File
@@ -1,331 +0,0 @@
body {
background-color: var(--bg);
margin: 0;
color: var(--fg);
}
.sidebar-container {
margin-top: 20px;
display: flex;
gap: 20px;
height: calc(100vh - 20px);
}
.dashboard-container {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
width: 100%;
padding-right: 25px;
overflow-y: auto;
height: 100%;
padding-bottom: 20px;
}
.summary-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: 100%;
}
.summary-card {
box-sizing: border-box;
height: 100px;
background-color: var(--card-bg);
padding: 1.5rem;
border-radius: 10px;
display: flex;
align-items: center;
width: 100%;
}
.summary-card h3 {
font-size: x-large;
margin: 0;
}
.summary-card p {
margin: 0;
color: var(--fg);
}
.summary-card svg {
margin-left: auto;
}
.fg {
color: var(--fg);
}
.online {
color: var(--online-ov);
}
.issues {
color: var(--issue);
}
.cpu {
color: var(--cpu);
}
.ram {
color: var(--memory);
}
.storage {
color: var(--storage);
}
.network {
color: var(--network);
}
.overview-container {
display: flex;
gap: 20px;
}
.performance-card {
background-color: var(--card-bg);
box-sizing: border-box;
padding: 1.5rem;
border-radius: 10px;
width: 100%;
}
.performance-stack {
display: flex;
position: relative;
}
.lc1,
.lc2,
.lc3 {
flex: 1;
position: relative;
}
.lc2,
.lc3 {
margin-left: -100%;
z-index: 1;
}
.lc1 {
color: var(--cpu);
}
.lc2 {
color: var(--memory);
}
.lc3 {
color: var(--network);
}
.chart-container {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr auto;
gap: 0.5rem;
width: 100%;
}
.y-axis {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-right: 0.5rem;
font-size: 0.75rem;
}
.y-axis div {
height: 100%;
text-align: right;
}
.chart-area {
display: flex;
flex-direction: column;
}
.performance-stack {
display: flex;
position: relative;
height: 300px;
}
.x-axis {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
padding-top: 0.5rem;
}
.x-axis div {
width: calc(100% / 24);
text-align: center;
}
.overview-card {
background-color: var(--card-bg);
box-sizing: border-box;
padding: 1.5rem;
border-radius: 10px;
width: 650px;
}
.overview-card h2 {
margin: 0;
padding: 0;
margin-bottom: 20px;
display: flex;
gap: 7px;
}
.overview-card h2 svg {
margin-top: auto;
margin-bottom: auto;
}
.overview-card div {
display: flex;
justify-content: space-between;
}
.overview-card div p {
margin-bottom: 8px;
font-size: 14px;
}
.search-container {
display: flex;
flex-wrap: wrap;
background-color: var(--card-bg);
box-sizing: border-box;
padding: 1.5rem;
border-radius: 10px;
width: 100%;
gap: 10px;
}
.search-input input {
appearance: auto;
background-color: var(--card-bg);
border: 1px solid rgb(226, 232, 240);
border-radius: 6px;
box-sizing: border-box;
color: var(--fg);
cursor: auto;
display: flex;
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
font-weight: 400;
line-height: 20px;
height: 40px;
width: 100%;
padding: 8px 12px 8px 40px;
margin: 0;
text-align: start;
}
.search-input input:focus {
border-color: var(--fg);
border-width: 2px;
outline: none;
}
.search-input {
position: relative;
width: 100%;
}
.search-input svg {
position: absolute;
top: 12px;
left: 12px;
}
.search-container button {
border: 0;
border-radius: 6px;
outline: none;
white-space: nowrap;
font-weight: bold;
}
.dashboard-container h3 {
padding: 0;
margin: 0;
}
@media (max-width: 1300px) {
.summary-container {
grid-template-columns: repeat(2, 1fr);
}
.overview-container {
flex-direction: column;
}
.overview-card {
width: 100%;
}
}
@media (max-width: 1000px) {
.dashboard-container {
padding-left: 25px;
}
}
.progress-bar {
display: block;
width: 100%;
height: 10px;
background-color: var(--progress-bg);
border-radius: 10px;
overflow: hidden;
}
.progress {
display: block;
height: 100%;
background-color: #4f46e5;
transition: width 0.3s ease;
border-radius: 10px;
}
.low {
color: var(--low);
}
.medium {
color: var(--medium);
}
.high {
color: var(--high);
}
.server-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25px;
}
@media (max-width: 1300px) {
.server-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 750px) {
.server-container {
grid-template-columns: repeat(1, 1fr);
}
}
-91
View File
@@ -1,91 +0,0 @@
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-Bold.ttf") format("truetype");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-Italic.ttf") format("truetype");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-BoldItalic.ttf") format("truetype");
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: "Roboto Condensed";
src: url("assets/font/Roboto_Condensed-ExtraLight.ttf") format("truetype");
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: "Roboto Condensed";
src: url("assets/font/Roboto_Condensed-ExtraLightItalic.ttf")
format("truetype");
font-weight: 200;
font-style: italic;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-MediumItalic.ttf") format("truetype");
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-SemiBoldItalic.ttf") format("truetype");
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-ExtraBold.ttf") format("truetype");
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: "Roboto";
src: url("assets/font/Roboto-ExtraBoldItalic.ttf") format("truetype");
font-weight: 800;
font-style: italic;
}
body {
font-family: "Roboto", "Roboto Condensed", sans-serif;
font-weight: normal;
font-style: normal;
}
.line-chart {
width: 100%;
height: 100%;
}
.line-chart-svg {
width: 100%;
height: 100%;
display: block;
color: currentColor;
}
-78
View File
@@ -1,78 +0,0 @@
body {
color: white;
background-color: black;
}
#login_wrapper {
position: relative;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
#background_img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
user-select: none;
}
#login_container {
background-color: rgba(138, 138, 138, 0.143);
border-radius: 20px;
backdrop-filter: blur(20px);
padding: 38px;
margin: auto;
width: max-content;
height: max-content;
z-index: 1;
}
.field {
background-color: rgba(138, 138, 138, 0.353);
border: none;
border-radius: 10px;
height: 40px;
width: 350px;
color: white;
padding-left: 10px;
padding-right: 10px;
}
.field:focus {
border: none;
outline: 1px solid white;
}
body {
margin: 0;
padding: 0;
}
h1 {
width: max-content;
margin-left: auto;
margin-right: auto;
}
button {
border: none;
outline: none;
background-color: white;
display: block;
margin-top: 20px;
margin-left: auto;
margin-right: auto;
width: max-content;
padding: 10px;
padding-left: 20px;
padding-right: 20px;
border-radius: 8px;
}
-140
View File
@@ -1,140 +0,0 @@
.server-container .server {
box-sizing: border-box;
background-color: var(--card-bg);
border-radius: 10px;
width: 100%;
height: 100%;
padding: 30px;
}
.server-heading-inner {
display: flex;
gap: 10px;
}
.server-icon {
padding: 0.5rem;
border-radius: 10px;
width: 20px;
height: 20px;
background-color: var(--server-icon-bg);
color: rgb(37 99 235);
margin-top: auto;
margin-bottom: auto;
}
.server-heading-info {
display: flex;
flex-direction: column;
gap: 7px;
}
.server-heading-info div div {
display: flex;
gap: 3px;
width: max-content;
padding-left: 10px;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
border-radius: 10px;
}
.server-heading-info div div svg {
margin-top: auto;
margin-bottom: auto;
}
.server-heading-info div div p {
margin-top: auto;
margin-bottom: auto;
font-size: 14px;
color: currentColor;
}
.server-heading-info div {
display: flex;
flex-direction: row;
gap: 10px;
}
.server-heading-info div p {
margin: 0;
font-size: 14.5px;
margin-top: auto;
margin-bottom: auto;
color: rgb(100 116 139);
}
.server-heading {
display: flex;
justify-content: space-between;
}
.server-heading button {
margin: 0;
border: 0;
height: max-content;
width: max-content;
padding: 8px;
align-items: center;
background-color: transparent;
border-radius: 7px;
margin-top: auto;
margin-bottom: auto;
color: var(--fg);
}
.server-heading button:hover {
background-color: var(--bg);
}
.server-monitor h2 {
margin: 0;
padding: 0;
margin-bottom: 20px;
display: flex;
gap: 7px;
}
.server-monitor h2 svg {
margin-top: auto;
margin-bottom: auto;
}
.server-monitor div {
display: flex;
justify-content: space-between;
}
.server-monitor div p {
margin-bottom: 8px;
font-size: 14px;
display: flex;
gap: 6px;
}
.All {
color: var(--fg);
background-color: var(--bg);
}
.Online {
color: var(--online);
background-color: var(--online-bg);
}
.Offline {
color: var(--offline);
background-color: var(--offline-bg);
}
.Warning {
color: var(--warning);
background-color: var(--warning-bg);
}
.Maintenance {
color: var(--maintenance);
background-color: var(--maintenance-bg);
}
-94
View File
@@ -1,94 +0,0 @@
body {
background-color: var(--bg);
margin: 0;
color: var(--fg);
}
.sidebar-container {
margin-top: 20px;
display: flex;
gap: 20px;
height: calc(100vh - 20px);
}
.settings-container {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
width: 100%;
padding-right: 25px;
overflow-y: auto;
height: 100%;
padding-bottom: 20px;
}
.settings-container {
background-color: var(--card-bg);
width: 100%;
height: 100%;
border-radius: 20px;
box-sizing: border-box;
padding: 25px;
}
.dropdown-none {
display: none !important;
}
.dropdown-button {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
padding: 10px;
gap: 10px;
border-radius: 10px;
border: 0;
outline: 0;
text-transform: capitalize;
background-color: var(--bg);
color: var(--fg);
}
.dropdown-chevron {
height: max-content;
width: max-content;
margin-top: auto;
margin-bottom: auto;
}
.dropdown {
display: flex;
flex-direction: column;
width: max-content;
background-color: transparent;
margin-top: 10px;
margin-left: auto;
margin-right: auto;
background-color: var(--bg);
color: var(--fg);
}
.dropdown button {
padding: 10px;
padding-left: 20px;
padding-right: 20px;
border: 0;
outline: 0;
background-color: transparent;
color: var(--fg);
}
.dropdown button:hover {
background-color: var(--card-bg);
}
.dropdown-button:hover {
background-color: var(--card-bg);
}
.dropdown-wrapper {
width: max-content;
}
-46
View File
@@ -1,46 +0,0 @@
#sidebar {
box-sizing: border-box;
background-color: var(--card-bg);
width: 300px;
height: calc(100vh - 40px);
border-radius: 20px;
margin-left: 25px;
padding: 20px;
}
@media (max-width: 1000px) {
#sidebar {
display: none;
height: 150px;
border-radius: 20px;
}
}
.sidebar-item {
text-decoration: none;
color: var(--fg);
font: inherit;
display: flex;
gap: 20px;
align-items: center;
padding-left: 20px;
border-radius: 20px;
}
.sidebar-item p {
text-decoration: none;
color: currentColor;
font: inherit;
font-size: larger;
font-weight: bold;
height: max-content;
}
.sidebar-item svg {
color: currentColor;
}
.sidebar-selected {
color: var(--fg-selected);
background-color: var(--bg-selected);
}
-111
View File
@@ -1,111 +0,0 @@
:root {
/* States */
--online: rgb(22 101 52);
--offline: rgb(153, 27, 27);
--warning: rgb(154, 52, 18);
--maintenance: rgb(30, 64, 175);
/* States bg */
--online-bg: rgb(220, 252, 231);
--offline-bg: rgb(254, 226, 226);
--warning-bg: rgb(255, 237, 213);
--maintenance-bg: rgb(219, 234, 254);
/* Overview data */
--online-ov: #16a34a;
--issue: rgb(234, 71, 12);
--cpu: rgb(37, 99, 235);
--memory: rgb(147, 51, 234);
--storage: rgb(234, 88, 12);
--network: rgb(22 163 74);
/* Usage levels */
--low: rgb(34, 197, 94);
--medium: rgb(234, 179, 8);
--high: rgb(239, 68, 68);
/* Misc */
--fg: #0f172a;
--bg: #f1f5f9;
--card-bg: #fff;
--progress-bg: rgb(226, 232, 240);
--server-icon-bg: rgb(219 234 254);
--fg-selected: #3f475a;
--bg-selected: #f1f5f9;
}
.dark {
/* States */
--online: rgb(34, 197, 94); /* bright green */
--offline: rgb(248, 113, 113); /* soft red */
--warning: rgb(246, 190, 50); /* yellow-orange */
--maintenance: rgb(147, 197, 253); /* light blue */
/* States bg */
--online-bg: rgba(22, 101, 52, 0.417); /* dark green */
--offline-bg: rgb(153, 27, 27, 0.417); /* dark red */
--warning-bg: rgba(102, 67, 21, 0.417); /* dark orange */
--maintenance-bg: rgb(30, 64, 175, 0.417); /* dark blue */
/* Overview data */
--online-ov: #16a34a;
--issue: rgb(234, 71, 12);
--cpu: rgb(37, 99, 235);
--memory: rgb(147, 51, 234);
--storage: rgb(234, 88, 12);
--network: rgb(22 163 74);
/* Usage levels */
--low: rgb(32, 232, 105); /* light green */
--medium: rgb(237, 201, 20); /* light yellow */
--high: rgb(232, 54, 54); /* red */
/* Misc */
--bg: #0f172a; /* dark navy */
--fg: #f8fafc; /* light text */
--card-bg: #141e2f; /* dark card background 141e2f */
--progress-bg: #27364d;
--server-icon-bg: #243756;
--fg-selected: #f8fafc;
--bg-selected: #0f172a;
}
.black {
/* States */
--online: rgb(0, 255, 128); /* neon green */
--offline: rgb(255, 77, 77); /* soft red */
--warning: rgb(255, 193, 7); /* bright yellow */
--maintenance: rgb(100, 181, 246); /* light blue */
/* States bg (dark, translucent overlays) */
--online-bg: rgba(0, 128, 64, 0.3); /* deep green */
--offline-bg: rgba(139, 0, 0, 0.3); /* dark red */
--warning-bg: rgba(153, 102, 0, 0.3); /* golden brown */
--maintenance-bg: rgba(25, 118, 210, 0.3); /* muted blue */
/* Overview data */
--online-ov: #00ff80; /* neon green */
--issue: rgb(255, 85, 0); /* warning orange */
--cpu: rgb(66, 165, 245); /* light blue */
--memory: rgb(200, 39, 228); /* purple */
--storage: rgb(255, 111, 0); /* orange */
--network: rgb(0, 200, 83); /* green */
/* Usage levels */
--low: rgb(11, 226, 86); /* neon green */
--medium: rgb(231, 215, 72); /* bright yellow */
--high: rgb(211, 56, 48); /* red */
/* Misc */
--bg: #000000; /* pure black */
--fg: #ffffff; /* pure white */
--card-bg: #141414; /* almost black card */
--progress-bg: #1a1a1a; /* very dark grey */
--server-icon-bg: #111111; /* dark grey */
--fg-selected: #f8fafc;
--bg-selected: #1a1a1a;
}
.error {
color: rgb(211, 56, 48);
}
-8
View File
@@ -1,8 +0,0 @@
await-holding-invalid-types = [
"generational_box::GenerationalRef",
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
"generational_box::GenerationalRefMut",
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
"dioxus_signals::Write",
{ path = "dioxus_signals::Write", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
]
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+1735
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "sentryx",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.4.1"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
}
}
+5
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+26
View File
@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
+103
View File
@@ -0,0 +1,103 @@
import Image from "next/image";
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
}
-69
View File
@@ -1,69 +0,0 @@
use dioxus::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct User {
username: String,
password: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Session {
uuid: String,
}
#[server]
pub async fn login(username: String, password: String) -> Result<String, ServerFnError> {
let users = crate::backend::get_mongo::<mongodb::bson::Document>("users").await?;
match users
.find_one(mongodb::bson::doc! {
"username": username,
"password": password,
})
.await?
{
Some(v) => {
let sessions = crate::backend::get_mongo::<Session>("sessions").await?;
if let mongodb::bson::Bson::ObjectId(id) = sessions
.insert_one(Session {
uuid: v.get_object_id("_id")?.to_hex(),
})
.await?
.inserted_id
{
Ok(id.to_hex())
} else {
Err(ServerFnError::new(format!("Unable to extract session id")))
}
}
None => Err(ServerFnError::new(format!("Invalid credentials"))),
}
}
#[cfg(feature = "server")]
pub async fn validate_session(id: String) -> bool {
use mongodb::bson::oid::ObjectId;
if let Ok(sessions) = crate::backend::get_mongo::<Session>("sessions").await {
if let Some(obj_id) = ObjectId::parse_str(id).ok() {
match sessions
.find_one(mongodb::bson::doc! {
"_id": obj_id,
})
.await
{
Ok(Some(_)) => true,
_ => false,
}
} else {
false
}
} else {
false
}
}
#[server]
pub async fn check_validate_session(id: String) -> Result<bool, ServerFnError> {
Ok(validate_session(id).await)
}
-73
View File
@@ -1,73 +0,0 @@
pub mod auth;
use crate::components::server::Server;
use dioxus::prelude::*;
#[cfg(feature = "server")]
use futures_util::stream::TryStreamExt;
#[cfg(feature = "server")]
const DB_URL: &'static str = "mongodb://admin:admin@localhost/";
#[cfg(feature = "server")]
pub async fn get_mongo<T: Send + Sync>(
collection: &str,
) -> Result<mongodb::Collection<T>, mongodb::error::Error> {
let client = mongodb::Client::with_uri_str(DB_URL).await?;
Ok(client.database("sentryx").collection(collection))
}
#[cfg(feature = "server")]
fn parse_search_query(input: &str) -> Vec<String> {
let re = regex::Regex::new(r#""([^"]+)"|(\S+)"#).unwrap();
re.captures_iter(input)
.filter_map(|cap| cap.get(1).or_else(|| cap.get(2)))
.map(|m| m.as_str().to_string())
.collect()
}
#[cfg(feature = "server")]
fn build_or_filter(input: &str, fields: &[&str]) -> mongodb::bson::Document {
let phrases = parse_search_query(input);
let mut or_clauses = Vec::new();
for phrase in phrases {
for &field in fields {
or_clauses.push(mongodb::bson::doc! {
field: mongodb::bson::Regex {
pattern: phrase.clone(),
options: "i".into(), // case-insensitive
}
});
}
}
mongodb::bson::doc! { "$or": or_clauses }
}
#[server]
pub async fn get_servers(session: String, search: String) -> Result<Vec<Server>, ServerFnError> {
if !auth::validate_session(session).await {
return Err(ServerFnError::new(format!("Invalid session")));
}
let servers = get_mongo::<Server>("servers").await?;
let filter = if search.trim().is_empty() {
mongodb::bson::doc! {}
} else {
build_or_filter(&search, &["name", "ip"])
};
let mut cursor = servers.find(filter).await?;
let mut svo = Vec::new();
while let Some(doc) = cursor.try_next().await? {
svo.push(doc);
}
Ok(svo)
}
-203
View File
@@ -1,203 +0,0 @@
pub mod server;
pub mod sidebar;
use dioxus::prelude::*;
#[component]
pub fn LineChart(
data: Vec<i32>,
max: Option<i32>,
start_opacity: Option<f32>,
end_opacity: Option<f32>,
class: Option<String>,
) -> Element {
let width = 300.0;
let height = 200.0;
let start_opacity = start_opacity.unwrap_or(0.4);
let end_opacity = end_opacity.unwrap_or(0.0);
let class_attr = match class {
Some(c) => format!("line-chart {}", c),
None => "line-chart".to_string(),
};
let max_y = max
.map(|v| v as f64)
.unwrap_or_else(|| *data.iter().max().unwrap_or(&100) as f64);
let points: Vec<(f64, f64)> = data
.iter()
.enumerate()
.map(|(i, &y)| {
let x = i as f64 / (data.len() - 1).max(1) as f64 * width;
let y = height - (y as f64 / max_y * height);
(x, y)
})
.collect();
let path_data = {
let mut d = format!("M {} {}", points[0].0, points[0].1);
for i in 0..points.len() - 1 {
let (x0, y0) = if i == 0 { points[0] } else { points[i - 1] };
let (x1, y1) = points[i];
let (x2, y2) = points[i + 1];
let (x3, y3) = if i + 2 < points.len() {
points[i + 2]
} else {
points[i + 1]
};
let cp1x = x1 + (x2 - x0) / 6.0;
let cp1y = y1 + (y2 - y0) / 6.0;
let cp2x = x2 - (x3 - x1) / 6.0;
let cp2y = y2 - (y3 - y1) / 6.0;
d += &format!(" C {} {}, {} {}, {} {}", cp1x, cp1y, cp2x, cp2y, x2, y2);
}
d
};
let fill_path_data = {
let mut d = format!("M {} {}", points[0].0, height);
for i in 0..points.len() - 1 {
let (x0, y0) = if i == 0 { points[0] } else { points[i - 1] };
let (x1, y1) = points[i];
let (x2, y2) = points[i + 1];
let (x3, y3) = if i + 2 < points.len() {
points[i + 2]
} else {
points[i + 1]
};
let cp1x = x1 + (x2 - x0) / 6.0;
let cp1y = y1 + (y2 - y0) / 6.0;
let cp2x = x2 - (x3 - x1) / 6.0;
let cp2y = y2 - (y3 - y1) / 6.0;
if i == 0 {
d += &format!(" L {} {}", points[0].0, points[0].1);
}
d += &format!(" C {} {}, {} {}, {} {}", cp1x, cp1y, cp2x, cp2y, x2, y2);
}
d += &format!(" L {} {}", points.last().unwrap().0, height);
d + " Z"
};
rsx! {
div {
class: "{class_attr}",
svg {
preserve_aspect_ratio: "none",
class: "line-chart-svg",
width: "{width}",
height: "{height}",
view_box: "0 0 {width} {height}",
xmlns: "http://www.w3.org/2000/svg",
defs {
linearGradient {
id: "gradient-fill",
x1: "0",
y1: "0",
x2: "0",
y2: "1",
stop {
offset: "0%",
stop_color: "currentColor",
stop_opacity: "{start_opacity}",
}
stop {
offset: "100%",
stop_color: "#currentColor",
stop_opacity: "{end_opacity}",
}
}
}
path {
class: "fill",
d: "{fill_path_data}",
fill: "url(#gradient-fill)"
}
path {
class: "line",
d: "{path_data}",
fill: "none",
stroke: "currentColor",
stroke_width: "2"
}
}
}
}
}
#[allow(unused)]
const RADIUS: f64 = 50.0;
#[allow(unused)]
const CIRCUMFERENCE: f64 = 2.0 * std::f64::consts::PI * RADIUS;
#[component]
pub fn RadialChart(percent: f64) -> Element {
let offset = CIRCUMFERENCE * (1.0 - percent.clamp(0.0, 1.0));
rsx! {
svg {
width: "140",
height: "140",
view_box: "0 0 120 120",
xmlns: "http://www.w3.org/2000/svg",
// Outline circle
circle {
cx: "60",
cy: "60",
r: "{RADIUS}",
stroke: "none",
stroke_width: "10.0",
fill: "none",
}
// Progress circle
circle {
cx: "60",
cy: "60",
r: "{RADIUS}",
stroke: "#4f46e5",
stroke_width: "10.5",
fill: "none",
stroke_dasharray: "{CIRCUMFERENCE}",
stroke_dashoffset: "{offset}",
transform: "rotate(-90 60 60)",
stroke_linecap: "round",
}
// Optional center text
text {
x: "60",
y: "65",
text_anchor: "middle",
font_size: "18",
fill: "#4f46e5",
font_family: "sans-serif",
"{(percent * 100.0).round()}%"
}
}
}
}
#[component]
pub fn ProgressBar(percent: u8, class: Option<String>) -> Element {
let class_attr = match class {
Some(c) => format!("progress-bar {}", c),
None => "progress-bar".to_string(),
};
rsx! {
span { class: "{class_attr}",
span { class: "progress", style: format!("width: {}%; background-color: currentColor;", percent.clamp(0, 100)) }
}
}
}
-99
View File
@@ -1,99 +0,0 @@
use crate::components::ProgressBar;
use dioxus::prelude::*;
const SERVER_CSS: Asset = asset!("/assets/styles/server.css");
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Status {
Online,
Offline,
Warning,
Maintenance,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Server {
pub name: String,
pub status: Status,
pub ip: String,
pub cpu: u8,
pub memory: u8,
pub storage: u8,
pub network: u8,
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Online => write!(f, "Online"),
Status::Warning => write!(f, "Warning"),
Status::Offline => write!(f, "Offline"),
Status::Maintenance => write!(f, "Maintenance"),
}
}
}
#[component]
pub fn ServerComponent(server: Server) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: SERVER_CSS }
div { class: "server",
div { class: "server-heading",
div { class: "server-heading-inner",
div { class: "server-icon",
lucide_dioxus::Server { size: 20, color: "currentColor" }
}
div { class: "server-heading-info",
h3 { "{server.name}" }
div {
div { class: "{server.status}",
lucide_dioxus::CircleCheckBig { size: 12, color: "currentColor" }
p { "{server.status}" }
}
p { "{server.ip}" }
}
}
}
button { lucide_dioxus::EllipsisVertical { size: 16, color: "currentColor" } }
}
div { class: "server-monitor",
div { p {
lucide_dioxus::Cpu { size: 16, color: "currentColor", class: "cpu" }
"CPU"
} p { "{server.cpu}%" } }
ProgressBar { percent: server.cpu, class: get_level(server.cpu) }
div { p {
lucide_dioxus::MemoryStick { size: 16, color: "currentColor", class: "ram" }
"Memory"
} p { "{server.memory}%" } }
ProgressBar { percent: server.memory, class: get_level(server.memory) }
div { p {
lucide_dioxus::HardDrive { size: 16, color: "currentColor", class: "storage" }
"Storage"
} p { "{server.storage}%" } }
ProgressBar { percent: server.storage, class: get_level(server.storage) }
div { p {
lucide_dioxus::Wifi { size: 16, color: "currentColor", class: "network" }
"Network"
} p { "{server.network}%" } }
ProgressBar { percent: server.network, class: get_level(server.network) }
}
}
}
}
fn get_level(i: u8) -> String {
if i > 80 {
String::from("high")
} else if i > 50 {
String::from("medium")
} else {
String::from("low")
}
}
-46
View File
@@ -1,46 +0,0 @@
use dioxus::prelude::*;
use crate::Route;
const SIDEBAR_CSS: Asset = asset!("/assets/styles/sidebar.css");
pub struct SideBarItem {
pub name: &'static str,
pub icon: fn() -> Element,
pub route: Route,
}
const SIDEBAR_ITEMS: &[SideBarItem] = &[
SideBarItem {
name: "Dashboard",
icon: || rsx!(lucide_dioxus::Gauge { size: 35 }),
route: Route::Dashboard {},
},
SideBarItem {
name: "Settings",
icon: || rsx!(lucide_dioxus::Settings { size: 35 }),
route: Route::Settings {},
},
];
#[component]
pub fn SideBar(selected: usize) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: SIDEBAR_CSS }
div {
id: "sidebar",
for (i, itm) in SIDEBAR_ITEMS.iter().enumerate() {
Link {
to: itm.route.clone(),
class: if i == selected {"sidebar-item sidebar-selected"} else {"sidebar-item"},
{(itm.icon)()}
p { "{itm.name}" }
}
}
}
}
}
-116
View File
@@ -1,116 +0,0 @@
use dioxus::prelude::*;
use crate::components::{server::ServerComponent, sidebar::SideBar, LineChart, ProgressBar};
use lucide_dioxus;
const DASHBOARD_CSS: Asset = asset!("/assets/styles/dashboard.css");
#[component]
pub fn Dashboard() -> Element {
let session = crate::utils::use_session();
let (servers, server_query) = session.use_servers();
rsx! {
document::Link { rel: "stylesheet", href: DASHBOARD_CSS }
div { class: "sidebar-container",
SideBar { selected: 0 }
div { class: "dashboard-container",
div { class: "summary-container",
div { class: "summary-card", class: "online",
div {
p { "Online Servers" }
h3 { "3" }
}
lucide_dioxus::CircleCheckBig { size: 32, color: "currentColor" }
}
div { class: "summary-card", class: "issues",
div {
p { "Issues" }
h3 { "2" }
}
lucide_dioxus::TriangleAlert { size: 32, color: "currentColor" }
}
div { class: "summary-card", class: "cpu",
div {
p { "Avg CPU Usage" }
h3 { "42%" }
}
lucide_dioxus::Cpu { size: 32, color: "currentColor" }
}
div { class: "summary-card", class: "ram",
div {
p { "Avg Memory" }
h3 { "42%" }
}
lucide_dioxus::MemoryStick { size: 32, color: "currentColor" }
}
}
div { class: "overview-container",
div { class: "performance-card",
div { class: "chart-container",
div { class: "y-axis",
{(0..=100).rev().step_by(25).map(|v| rsx!(div { "{v}" }))}
}
div { class: "chart-area",
div { class: "performance-stack",
LineChart { data: vec![30, 35, 23, 21, 32, 40, 53, 61, 47, 36, 26, 18], max: 100, class: "lc1" }
LineChart { data: vec![47, 50, 60, 30, 50], max: 100, class: "lc2" }
LineChart { data: vec![23, 50, 40, 50, 52], max: 100, class: "lc3" }
}
div { class: "x-axis",
{(0..24).step_by(2).map(|h| rsx!(div { "{h}h" }))}
}
}
}
}
div { class: "overview-card",
h2 {
lucide_dioxus::Activity { size: 22, color: "currentColor" }
"System Overview"
}
div { p { "Average CPU" } p { "10%" } }
ProgressBar { percent: 10, class: "cpu" }
div { p { "Average Memory" } p { "24%" } }
ProgressBar { percent: 10, class: "ram" }
div { p { "Average Storage" } p { "24%" } }
ProgressBar { percent: 10, class: "storage" }
div { p { "Average Network" } p { "24%" } }
ProgressBar { percent: 10, class: "network" }
}
}
div { class: "search-container",
div { class: "search-input",
lucide_dioxus::Search { size: 16, color: "currentColor" }
input {
onchange: {let mut server_query = server_query.clone(); move |e: Event<FormData>| {server_query.set(e.value())}},
placeholder: "Search servers by name, location, or IP..." }
}
button { class: "All", "All (4)" }
button { class: "Online", "Online (3)" }
button { class: "Warning", "Warning (1)" }
button { class: "Offline", "Offline (1)" }
button { class: "Maintenance", "Maintenance (1)" }
}
div { class: "server-container",
for s in (*servers.read()).clone() {
ServerComponent { server: s }
}
}
}
}
}
}
-115
View File
@@ -1,115 +0,0 @@
use dioxus::prelude::*;
use crate::{
backend::{self},
utils::{self},
};
const LOGIN_CSS: Asset = asset!("/assets/styles/login.css");
#[component]
pub fn Login() -> Element {
let username = use_signal(|| String::new());
let password = use_signal(|| String::new());
let error: Signal<Option<String>> = use_signal(|| None);
let session = utils::use_session();
// Check for existing session on mount
use_effect({
let session = session.clone();
move || {
let session = session.clone();
spawn(async move {
// Optional: verify with the server if session is actually valid
if backend::auth::check_validate_session(session.get_id())
.await
.unwrap_or(false)
{
utils::redirect("/dashboard");
}
});
}
});
let is_disabled = {
let username = username.clone();
let password = password.clone();
move || username().is_empty() || password().is_empty()
};
let log_in = {
let username = username.clone();
let password = password.clone();
let mut error = error.clone();
move || async move {
if username().is_empty() || password().is_empty() {
return;
}
error.set(None);
match backend::auth::login(username.read().clone(), password.read().clone()).await {
Ok(id) => {
utils::set_cookie("session_id", &id);
utils::redirect("/dashboard");
}
Err(e) => error.set(Some(
e.to_string().replace("error running server function: ", ""),
)),
}
}
};
rsx! {
document::Link { rel: "stylesheet", href: LOGIN_CSS }
div {
id: "login_wrapper",
onkeypress: move |e: Event<KeyboardData>| async move {
if e.key() == Key::Enter {
log_in().await;
}
},
tabindex: "0",
img {
id: "background_img",
src: asset!("assets/background.svg")
}
div {
id: "login_container",
h1 { "Login" }
div {
p { "Username" }
input {
class: "field",
oninput: {
let mut username = username.clone();
move |e: Event<FormData>| {username.set(e.value())}
},
}
}
div{
p { "Password" }
input {
class: "field",
oninput: {
let mut password = password.clone();
move |e: Event<FormData>| {password.set(e.value())}
},
}
}
if let Some(e) = &*error.read() {
p { class: "error", "{e}" }
}
button {
disabled: is_disabled(),
onclick: move |_| log_in(),
"Login"
}
}
}
}
}
-62
View File
@@ -1,62 +0,0 @@
mod backend;
mod components;
mod dashboard;
mod login;
mod settings;
mod utils;
use dioxus::prelude::*;
use web_sys::window;
use dashboard::Dashboard;
use login::Login;
use settings::Settings;
#[derive(Debug, Clone, Routable, PartialEq)]
#[rustfmt::skip]
enum Route {
#[route("/")]
Login {},
#[route("/dashboard")]
Dashboard {},
#[route("/settings")]
Settings {},
}
const FAVICON: Asset = asset!("/assets/favicon.ico");
const FONT_CSS: Asset = asset!("/assets/styles/font.css");
const THEME_CSS: Asset = asset!("/assets/styles/theme.css");
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let theme = use_signal(|| String::from("light"));
use_effect({
let mut theme = theme.clone();
move || {
theme.set(utils::get_cookie("theme").unwrap_or_default());
}
});
use_effect({
let theme = theme.clone();
move || {
if let Some(document) = window().and_then(|w| w.document()) {
if let Some(html) = document.document_element() {
html.set_class_name(&*theme.read());
}
}
}
});
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: FONT_CSS }
document::Link { rel: "stylesheet", href: THEME_CSS }
Router::<Route> { }
}
}
-68
View File
@@ -1,68 +0,0 @@
use dioxus::prelude::*;
use crate::{components::sidebar::SideBar, utils};
pub enum Property {
Dropdown(&'static [&'static str]),
}
const SETTINGS_CSS: Asset = asset!("/assets/styles/settings.css");
const OPTIONS: &[(&str, Property, bool)] = &[(
"theme",
Property::Dropdown(&["light", "dark", "black"]),
true,
)];
#[component]
pub fn Settings() -> Element {
rsx! {
document::Link { rel: "stylesheet", href: SETTINGS_CSS }
div { class: "sidebar-container",
SideBar { selected: 1 }
div { class: "settings-container",
div {
for (name, prop, needs_refresh) in OPTIONS {
match prop {
Property::Dropdown(opts) => {
rsx!{
div { class: "dropdown-wrapper",
button { class: "dropdown-button", onclick: |_| {
let Some(document) = web_sys::window().and_then(|w| w.document()) else {
return;
};
let e = document.get_element_by_id(name).unwrap();
if e.class_name() == "dropdown" {
e.set_class_name("dropdown dropdown-none");
} else {
e.set_class_name("dropdown");
}
},
"{name}"
lucide_dioxus::ChevronDown { size: 16, class: "chevron", color: "currentColor" }
}
div {
id: "{name}",
class: "dropdown dropdown-none",
for o in opts {
button { onclick: move |_| {
utils::set_cookie(name, &o);
if *needs_refresh {
if let Some(win) = web_sys::window() {
win.location().reload().unwrap();
}
}
}, "{o}" }
}
}
}
}
}
}
}
}
}
}
}
}
-116
View File
@@ -1,116 +0,0 @@
use dioxus::prelude::*;
use std::str::FromStr;
use wasm_bindgen::JsCast;
use web_sys::{window, HtmlDocument};
use crate::{backend, components::server::Server};
pub fn set_cookie(name: &str, value: &str) {
let Some(document) = window().and_then(|w| w.document()) else {
return;
};
let Ok(html_doc) = document.dyn_into::<HtmlDocument>() else {
return;
};
let cookie = format!("{name}={value}; path=/");
let _ = html_doc.set_cookie(&cookie);
}
pub fn get_cookie(name: &str) -> Option<String> {
let Some(document) = window().and_then(|w| w.document()) else {
return None;
};
let Ok(html_doc) = document.dyn_into::<HtmlDocument>() else {
return None;
};
let Ok(cookie_string) = html_doc.cookie() else {
return None;
};
for cookie in cookie_string.split(';') {
let cookie = cookie.trim();
if let Some((k, v)) = cookie.split_once('=') {
if k == name {
return Some(v.to_string());
}
}
}
None
}
pub fn use_cookie<T: FromStr + 'static>(name: &str, default: fn() -> T) -> Signal<T> {
let name = name.to_string();
let signal = use_signal(default);
use_effect({
let mut signal = signal.clone();
move || {
if let Some(c) = get_cookie(&name) {
if let Ok(cv) = T::from_str(&c) {
signal.set(cv);
}
}
}
});
signal
}
pub fn redirect(target: &str) {
web_sys::window()
.unwrap()
.location()
.set_href(target)
.unwrap();
}
#[derive(Clone)]
pub struct Session(Signal<String>);
pub fn use_session() -> Session {
Session(use_cookie("session_id", || String::new()))
}
impl Session {
pub fn get_id(&self) -> String {
self.0.read().clone()
}
pub fn use_servers(&self) -> (Signal<Vec<Server>>, Signal<String>) {
let query = use_signal(|| String::new());
let servers = use_signal(|| Vec::new());
let id = self.0.clone();
use_effect(move || {
let query_value = query.read().clone();
let mut servers = servers.clone();
spawn(async move {
if let Ok(v) = request(backend::get_servers(id.read().clone(), query_value).await) {
servers.set(v);
}
});
});
(servers, query)
}
}
pub fn request<T>(o: Result<T, ServerFnError>) -> Result<T, ServerFnError> {
match o {
Err(e) => match e {
ServerFnError::ServerError(e) => {
if e == "Invalid session" {
redirect("/");
Err(ServerFnError::ServerError(e))
} else {
Err(ServerFnError::ServerError(e))
}
}
_ => Err(e),
},
_ => o,
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}