feat: Implement dees-statsgrid in DCRouter UI for enhanced stats visualization
- Added new readme.statsgrid.md outlining the implementation plan for dees-statsgrid component. - Replaced custom stats cards in ops-view-overview.ts and ops-view-network.ts with dees-statsgrid for better visualization. - Introduced consistent color scheme for success, warning, error, and info states. - Enhanced interactive features including click actions, context menus, and real-time updates. - Developed ops-view-emails.ts for email management with features like composing, searching, and viewing emails. - Integrated mock data generation for emails and network requests to facilitate testing. - Added responsive design elements and improved UI consistency across components.
This commit is contained in:
@ -20,7 +20,7 @@
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tswatch": "^2.0.1",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/node": "^24.0.0",
|
||||
"node-forge": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -29,7 +29,7 @@
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
"@api.global/typedsocket": "^3.0.0",
|
||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||
"@design.estate/dees-catalog": "^1.8.0",
|
||||
"@design.estate/dees-catalog": "^1.8.1",
|
||||
"@design.estate/dees-element": "^2.0.42",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
|
180
pnpm-lock.yaml
generated
180
pnpm-lock.yaml
generated
@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^6.4.1
|
||||
version: 6.4.1
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.1
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.0.42
|
||||
version: 2.0.42
|
||||
@ -130,8 +130,8 @@ importers:
|
||||
specifier: ^2.0.1
|
||||
version: 2.1.0
|
||||
'@types/node':
|
||||
specifier: ^22.15.30
|
||||
version: 22.15.30
|
||||
specifier: ^24.0.0
|
||||
version: 24.0.0
|
||||
node-forge:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
@ -344,8 +344,8 @@ packages:
|
||||
'@dabh/diagnostics@2.0.3':
|
||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||
|
||||
'@design.estate/dees-catalog@1.8.0':
|
||||
resolution: {integrity: sha512-8xUkp+fPjXR9/GM5fvc+eHogpirkcmW4Ao8vNLgIomejoLRamtIbICJQaVmpra20XJqPkOcnvHwUHyhLckwYJA==}
|
||||
'@design.estate/dees-catalog@1.8.1':
|
||||
resolution: {integrity: sha512-luYrGMK5APzw7GgXSe4UpFE5oC3vwzKTTB0etH3o6Au1nxvllhDcWr6S5UkB+WL9lroslBcK8Icyhiq8kIxEHg==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.27':
|
||||
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
||||
@ -737,68 +737,68 @@ packages:
|
||||
'@mongodb-js/saslprep@1.2.2':
|
||||
resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.70':
|
||||
resolution: {integrity: sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==}
|
||||
'@napi-rs/canvas-android-arm64@0.1.71':
|
||||
resolution: {integrity: sha512-cxi3VCotIOS9kNFQI7dcysbVJi106pxryVY1Hi85pX+ZeqahRyeqc/NsLaZ998Ae99+F3HI5X/39G1Y/Byrf0A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.70':
|
||||
resolution: {integrity: sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==}
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.71':
|
||||
resolution: {integrity: sha512-7Y4D/6vIuMLYsVNtRM/w2j0+fB1GyqeOxc7I0BTx8eLP1S6BZE2Rj6zJfdG+zmLEOW0IlHa+VQq1q2MUAjW84w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.70':
|
||||
resolution: {integrity: sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==}
|
||||
'@napi-rs/canvas-darwin-x64@0.1.71':
|
||||
resolution: {integrity: sha512-Z0IUqxclrYdfVt/SK9nKCzUHTOXKTWiygtO71YCzs0OtxKdNI7GJRJdYG48wXZEDQ/pqTF4F7Ifgtidfc2tYpg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70':
|
||||
resolution: {integrity: sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==}
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
|
||||
resolution: {integrity: sha512-KlpqqCASak5ruY+UIolJgmhMZ9Pa2o1QyaNu648L8sz4WNBbNa+aOT60XCLCL1VIKLv11B3MlNgiOHoYNmDhXQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.70':
|
||||
resolution: {integrity: sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==}
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.71':
|
||||
resolution: {integrity: sha512-bdGZCGu8YQNAiu3nkIVVUp6nIn6fPd36IuZsLXTG027E52KyIuZ3obCxehSwjDIUNkFWvmff5D6JYfWwAoioEw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
|
||||
resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==}
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.71':
|
||||
resolution: {integrity: sha512-1R5sMWe9ur8uM+hAeylBwG0b6UHDR+iWQNgzXmF9vbBYRooQvmDWqpcgytKLJAC0vnWhIkKwqd7yExn7cwczmg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
|
||||
resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==}
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
|
||||
resolution: {integrity: sha512-xjjKsipueuG+LdKIk6/uAlqdo+rzGcmNpTZPXdakIT1sHX4NNSnQTzjRaj9Gh96Czjd9G89UWR0KIlE7fwOgFA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
|
||||
resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==}
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.71':
|
||||
resolution: {integrity: sha512-3s6YpklXDB4OeeULG1XTRyKrKAOo7c3HHEqM9A6N4STSjMaJtzmpp7tB/JTvAFeOeFte6gWN8IwC+7AjGJ6MpQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.70':
|
||||
resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==}
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.71':
|
||||
resolution: {integrity: sha512-5v9aCLzCXw7u10ray5juQMdl7TykZSn1X5AIGYwBvTAcKSgrqaR9QkRxp1Lqk3njQmFekOW1SFN9bZ/i/6y6kA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
|
||||
resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==}
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.71':
|
||||
resolution: {integrity: sha512-oJughk6xjsRIr0Rd9EqjmZmhIMkvcPuXgr3MNn2QexTqn+YFOizrwHS5ha0BDfFl7TEGRvwaDUXBQtu8JKXb8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.70':
|
||||
resolution: {integrity: sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==}
|
||||
'@napi-rs/canvas@0.1.71':
|
||||
resolution: {integrity: sha512-92ybDocKl6JM48ZpYbj+A7Qt45IaTABDk0y3sDecEQfgdhfNzJtEityqNHoCZ4Vty2dldPkJhxgvOnbrQMXTTA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@oozcitak/dom@1.15.10':
|
||||
@ -1661,8 +1661,8 @@ packages:
|
||||
'@types/node@18.19.111':
|
||||
resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==}
|
||||
|
||||
'@types/node@22.15.30':
|
||||
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
|
||||
'@types/node@24.0.0':
|
||||
resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==}
|
||||
|
||||
'@types/pidusage@2.0.5':
|
||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
||||
@ -2963,8 +2963,8 @@ packages:
|
||||
leac@0.6.0:
|
||||
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||
|
||||
lenis@1.3.3:
|
||||
resolution: {integrity: sha512-DOopj/UKHS54E9l2g4BOpDUvsyvkd1zkv+ECtHxQ9Fto8ozzKSz7MccqT+KOyG0ABA/OHXZ7l9INx0peUoQ8rQ==}
|
||||
lenis@1.3.4:
|
||||
resolution: {integrity: sha512-WIGk8wiV2ABm/T7M+NC+tAV8fjzNJD1J4z11aZ3mTtx7WAZX/4QdCNhBO0g/TqXISA+/3hTbzrPC4FW1nhoNMQ==}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': '>=3.0.0'
|
||||
react: '>=17.0.0'
|
||||
@ -4194,8 +4194,8 @@ packages:
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
undici-types@7.8.0:
|
||||
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||
|
||||
undici@7.10.0:
|
||||
resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
|
||||
@ -5075,7 +5075,7 @@ snapshots:
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
|
||||
'@design.estate/dees-catalog@1.8.0':
|
||||
'@design.estate/dees-catalog@1.8.1':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.2
|
||||
'@design.estate/dees-element': 2.0.42
|
||||
@ -5127,7 +5127,7 @@ snapshots:
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
'@push.rocks/websetup': 3.0.19
|
||||
'@push.rocks/webstore': 2.0.20
|
||||
lenis: 1.3.3
|
||||
lenis: 1.3.4
|
||||
lit: 3.3.0
|
||||
sweet-scroll: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
@ -5487,48 +5487,48 @@ snapshots:
|
||||
dependencies:
|
||||
sparse-bitfield: 3.0.3
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.70':
|
||||
'@napi-rs/canvas-android-arm64@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.70':
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.70':
|
||||
'@napi-rs/canvas-darwin-x64@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70':
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.70':
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.70':
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.71':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.70':
|
||||
'@napi-rs/canvas@0.1.71':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.70
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.70
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.70
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.70
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.70
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.70
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.70
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.70
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.70
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.70
|
||||
'@napi-rs/canvas-android-arm64': 0.1.71
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.71
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.71
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.71
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.71
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.71
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.71
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.71
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.71
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.71
|
||||
optional: true
|
||||
|
||||
'@oozcitak/dom@1.15.10':
|
||||
@ -5784,7 +5784,6 @@ snapshots:
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
@ -5793,7 +5792,6 @@ snapshots:
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
@ -7046,27 +7044,27 @@ snapshots:
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/body-parser@1.19.5':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/cors@2.8.18':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@ -7078,7 +7076,7 @@ snapshots:
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
@ -7086,7 +7084,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@5.0.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
@ -7103,30 +7101,30 @@ snapshots:
|
||||
|
||||
'@types/from2@2.3.5':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/fs-extra@9.0.13':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/glob@7.2.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/gunzip-maybe@1.4.2':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@ -7148,16 +7146,16 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/jsonwebtoken@9.0.9':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/mailparser@3.4.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
@ -7176,20 +7174,20 @@ snapshots:
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/node@18.19.111':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.15.30':
|
||||
'@types/node@24.0.0':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
undici-types: 7.8.0
|
||||
|
||||
'@types/pidusage@2.0.5': {}
|
||||
|
||||
@ -7205,30 +7203,30 @@ snapshots:
|
||||
|
||||
'@types/s3rver@3.7.4':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.4
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/symbol-tree@3.2.5': {}
|
||||
|
||||
'@types/tar-stream@2.2.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
@ -7252,18 +7250,18 @@ snapshots:
|
||||
|
||||
'@types/whatwg-url@8.2.2':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
|
||||
'@types/which@3.0.4': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@ -7835,7 +7833,7 @@ snapshots:
|
||||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.18
|
||||
'@types/node': 22.15.30
|
||||
'@types/node': 24.0.0
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
@ -8663,7 +8661,7 @@ snapshots:
|
||||
|
||||
leac@0.6.0: {}
|
||||
|
||||
lenis@1.3.3: {}
|
||||
lenis@1.3.4: {}
|
||||
|
||||
libbase64@1.3.0: {}
|
||||
|
||||
@ -9473,7 +9471,7 @@ snapshots:
|
||||
|
||||
pdfjs-dist@4.10.38:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.70
|
||||
'@napi-rs/canvas': 0.1.71
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
@ -10168,7 +10166,7 @@ snapshots:
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
undici-types@7.8.0: {}
|
||||
|
||||
undici@7.10.0: {}
|
||||
|
||||
|
46
readme.statsgrid.md
Normal file
46
readme.statsgrid.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Plan: Implement dees-statsgrid in DCRouter UI
|
||||
|
||||
Command to reread CLAUDE.md: `Read /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
||||
|
||||
## Overview
|
||||
Replace the current stats cards with the new dees-statsgrid component from @design.estate/dees-catalog for better visualization and consistency.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Update Overview View (`ops-view-overview.ts`)
|
||||
- Replace the custom stats cards with dees-statsgrid
|
||||
- Use appropriate tile types for different metrics:
|
||||
- `gauge` for CPU and Memory usage
|
||||
- `number` for Active Connections, Total Requests, etc.
|
||||
- `trend` for time-series data like requests over time
|
||||
|
||||
### 2. Update Network View (`ops-view-network.ts`)
|
||||
- Replace the current stats cards section with dees-statsgrid
|
||||
- Configure tiles for:
|
||||
- Active Connections (number)
|
||||
- Requests/sec (number with trend)
|
||||
- Throughput In/Out (number with units)
|
||||
- Protocol distribution (percentage)
|
||||
|
||||
### 3. Create Consistent Color Scheme
|
||||
- Success/Normal: #22c55e (green)
|
||||
- Warning: #f59e0b (amber)
|
||||
- Error/Critical: #ef4444 (red)
|
||||
- Info: #3b82f6 (blue)
|
||||
|
||||
### 4. Add Interactive Features
|
||||
- Click actions to show detailed views
|
||||
- Context menu for refresh, export, etc.
|
||||
- Real-time updates from metrics data
|
||||
|
||||
### 5. Integration Points
|
||||
- Connect to existing appstate for data
|
||||
- Use MetricsManager data for real values
|
||||
- Update on the 1-second refresh interval
|
||||
|
||||
## Benefits
|
||||
- Consistent UI component usage
|
||||
- Better visual hierarchy
|
||||
- Built-in responsive design
|
||||
- More visualization options (gauges, trends)
|
||||
- Reduced custom CSS maintenance
|
@ -1,6 +1,7 @@
|
||||
export * from './ops-dashboard.js';
|
||||
export * from './ops-view-overview.js';
|
||||
export * from './ops-view-stats.js';
|
||||
export * from './ops-view-network.js';
|
||||
export * from './ops-view-emails.js';
|
||||
export * from './ops-view-logs.js';
|
||||
export * from './ops-view-config.js';
|
||||
export * from './ops-view-security.js';
|
||||
|
@ -13,7 +13,8 @@ import {
|
||||
|
||||
// Import view components
|
||||
import { OpsViewOverview } from './ops-view-overview.js';
|
||||
import { OpsViewStats } from './ops-view-stats.js';
|
||||
import { OpsViewNetwork } from './ops-view-network.js';
|
||||
import { OpsViewEmails } from './ops-view-emails.js';
|
||||
import { OpsViewLogs } from './ops-view-logs.js';
|
||||
import { OpsViewConfig } from './ops-view-config.js';
|
||||
import { OpsViewSecurity } from './ops-view-security.js';
|
||||
@ -84,8 +85,12 @@ export class OpsDashboard extends DeesElement {
|
||||
element: OpsViewOverview,
|
||||
},
|
||||
{
|
||||
name: 'Statistics',
|
||||
element: OpsViewStats,
|
||||
name: 'Network',
|
||||
element: OpsViewNetwork,
|
||||
},
|
||||
{
|
||||
name: 'Emails',
|
||||
element: OpsViewEmails,
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
|
697
ts_web/elements/ops-view-emails.ts
Normal file
697
ts_web/elements/ops-view-emails.ts
Normal file
@ -0,0 +1,697 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-emails': OpsViewEmails;
|
||||
}
|
||||
}
|
||||
|
||||
interface IEmail {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string[];
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
html?: string;
|
||||
attachments?: Array<{
|
||||
filename: string;
|
||||
size: number;
|
||||
contentType: string;
|
||||
}>;
|
||||
date: number;
|
||||
read: boolean;
|
||||
folder: 'inbox' | 'sent' | 'draft' | 'trash';
|
||||
flags?: string[];
|
||||
messageId?: string;
|
||||
inReplyTo?: string;
|
||||
}
|
||||
|
||||
@customElement('ops-view-emails')
|
||||
export class OpsViewEmails extends DeesElement {
|
||||
@state()
|
||||
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
|
||||
|
||||
@state()
|
||||
private emails: IEmail[] = [];
|
||||
|
||||
@state()
|
||||
private selectedEmail: IEmail | null = null;
|
||||
|
||||
@state()
|
||||
private showCompose = false;
|
||||
|
||||
@state()
|
||||
private isLoading = false;
|
||||
|
||||
@state()
|
||||
private searchTerm = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadEmails();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.emailContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
gap: 24px;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.folderList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.folderItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.folderItem:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.folderItem.selected {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.folderIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.folderLabel {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.folderCount {
|
||||
background: #e0e0e0;
|
||||
color: #666;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.folderItem.selected .folderCount {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.emailToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.emailList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emailPreview {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emailHeader {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.emailSubject {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.emailMeta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.emailBody {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emailActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.composeModal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.composeContent {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.composeHeader {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.composeTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.composeForm {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.composeActions {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 18px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Emails</ops-sectionheading>
|
||||
|
||||
<div class="emailContainer">
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<dees-button @click=${() => this.showCompose = true} type="highlighted">
|
||||
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
|
||||
Compose
|
||||
</dees-button>
|
||||
|
||||
<div class="folderList">
|
||||
${this.renderFolderItem('inbox', 'inbox', 'Inbox', this.getEmailCount('inbox'))}
|
||||
${this.renderFolderItem('sent', 'paperPlane', 'Sent', this.getEmailCount('sent'))}
|
||||
${this.renderFolderItem('draft', 'file', 'Drafts', this.getEmailCount('draft'))}
|
||||
${this.renderFolderItem('trash', 'trash', 'Trash', this.getEmailCount('trash'))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="mainContent">
|
||||
<!-- Toolbar -->
|
||||
<div class="emailToolbar">
|
||||
<dees-input-text
|
||||
class="searchBox"
|
||||
placeholder="Search emails..."
|
||||
.value=${this.searchTerm}
|
||||
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
||||
>
|
||||
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
|
||||
</dees-input-text>
|
||||
|
||||
<dees-button @click=${() => this.refreshEmails()}>
|
||||
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : html`<dees-icon name="arrowsRotate"></dees-icon>`}
|
||||
</dees-button>
|
||||
|
||||
<dees-button @click=${() => this.markAllAsRead()}>
|
||||
<dees-icon name="envelopeOpen"></dees-icon>
|
||||
Mark all read
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<!-- Email List or Preview -->
|
||||
${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailList()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compose Modal -->
|
||||
${this.showCompose ? this.renderComposeModal() : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderFolderItem(folder: string, icon: string, label: string, count: number) {
|
||||
return html`
|
||||
<div
|
||||
class="folderItem ${this.selectedFolder === folder ? 'selected' : ''}"
|
||||
@click=${() => this.selectFolder(folder as any)}
|
||||
>
|
||||
<dees-icon class="folderIcon" name="${icon}"></dees-icon>
|
||||
<span class="folderLabel">${label}</span>
|
||||
${count > 0 ? html`<span class="folderCount">${count}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailList() {
|
||||
const filteredEmails = this.getFilteredEmails();
|
||||
|
||||
if (filteredEmails.length === 0) {
|
||||
return html`
|
||||
<div class="emptyState">
|
||||
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
|
||||
<div class="emptyText">No emails in ${this.selectedFolder}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="emailList">
|
||||
<dees-table
|
||||
.data=${filteredEmails}
|
||||
.displayFunction=${(email: IEmail) => ({
|
||||
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" style="color: ${email.read ? '#999' : '#1976d2'}"></dees-icon>`,
|
||||
From: email.from,
|
||||
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
|
||||
Date: this.formatDate(email.date),
|
||||
'Attach': html`
|
||||
${email.attachments?.length ? html`<dees-icon name="paperclip" style="color: #666"></dees-icon>` : ''}
|
||||
`,
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Read',
|
||||
iconName: 'eye',
|
||||
type: ['doubleClick', 'inRow'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.selectedEmail = actionData.item;
|
||||
if (!actionData.item.read) {
|
||||
this.markAsRead(actionData.item.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Reply',
|
||||
iconName: 'reply',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.replyToEmail(actionData.item);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Forward',
|
||||
iconName: 'share',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.forwardEmail(actionData.item);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
this.deleteEmail(actionData.item.id);
|
||||
}
|
||||
}
|
||||
]}
|
||||
.selectionMode=${'single'}
|
||||
></dees-table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailPreview() {
|
||||
if (!this.selectedEmail) return '';
|
||||
|
||||
return html`
|
||||
<div class="emailPreview">
|
||||
<div class="emailHeader">
|
||||
<div class="emailSubject">${this.selectedEmail.subject}</div>
|
||||
<div class="emailMeta">
|
||||
<span><strong>From:</strong> ${this.selectedEmail.from}</span>
|
||||
<span><strong>To:</strong> ${this.selectedEmail.to.join(', ')}</span>
|
||||
<span><strong>Date:</strong> ${this.formatDate(this.selectedEmail.date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emailBody">
|
||||
${this.selectedEmail.html ?
|
||||
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
|
||||
html`<pre style="white-space: pre-wrap; font-family: inherit;">${this.selectedEmail.body}</pre>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="emailActions">
|
||||
<dees-button @click=${() => this.selectedEmail = null}>
|
||||
<dees-icon name="arrowLeft"></dees-icon>
|
||||
Back
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
|
||||
<dees-icon name="reply"></dees-icon>
|
||||
Reply
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
|
||||
<dees-icon name="replyAll"></dees-icon>
|
||||
Reply All
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
|
||||
<dees-icon name="share"></dees-icon>
|
||||
Forward
|
||||
</dees-button>
|
||||
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
|
||||
<dees-icon name="trash"></dees-icon>
|
||||
Delete
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderComposeModal() {
|
||||
return html`
|
||||
<div class="composeModal" @click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) this.showCompose = false;
|
||||
}}>
|
||||
<div class="composeContent">
|
||||
<div class="composeHeader">
|
||||
<div class="composeTitle">New Email</div>
|
||||
<dees-button @click=${() => this.showCompose = false} type="ghost">
|
||||
<dees-icon name="xmark"></dees-icon>
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="composeForm">
|
||||
<dees-form @formData=${(e: CustomEvent) => this.sendEmail(e.detail)}>
|
||||
<dees-input-tags
|
||||
key="to"
|
||||
label="To"
|
||||
placeholder="Enter recipient email addresses..."
|
||||
required
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
key="cc"
|
||||
label="CC"
|
||||
placeholder="Enter CC recipients..."
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
key="bcc"
|
||||
label="BCC"
|
||||
placeholder="Enter BCC recipients..."
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-text
|
||||
key="subject"
|
||||
label="Subject"
|
||||
placeholder="Enter email subject..."
|
||||
required
|
||||
></dees-input-text>
|
||||
|
||||
<dees-editor
|
||||
key="body"
|
||||
label="Message"
|
||||
.mode=${'markdown'}
|
||||
.height=${300}
|
||||
required
|
||||
></dees-editor>
|
||||
|
||||
<dees-input-fileupload
|
||||
key="attachments"
|
||||
label="Attachments"
|
||||
multiple
|
||||
></dees-input-fileupload>
|
||||
|
||||
<div class="composeActions">
|
||||
<dees-button @click=${() => this.showCompose = false} type="secondary">
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-form-submit>
|
||||
<dees-icon name="paperPlane"></dees-icon>
|
||||
Send Email
|
||||
</dees-form-submit>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getFilteredEmails(): IEmail[] {
|
||||
let emails = this.emails.filter(e => e.folder === this.selectedFolder);
|
||||
|
||||
if (this.searchTerm) {
|
||||
const search = this.searchTerm.toLowerCase();
|
||||
emails = emails.filter(e =>
|
||||
e.subject.toLowerCase().includes(search) ||
|
||||
e.from.toLowerCase().includes(search) ||
|
||||
e.body.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
return emails.sort((a, b) => b.date - a.date);
|
||||
}
|
||||
|
||||
private getEmailCount(folder: string): number {
|
||||
return this.emails.filter(e => e.folder === folder && !e.read).length;
|
||||
}
|
||||
|
||||
private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') {
|
||||
this.selectedFolder = folder;
|
||||
this.selectedEmail = null;
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = diff / (1000 * 60 * 60);
|
||||
|
||||
if (hours < 24) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (hours < 168) { // 7 days
|
||||
return date.toLocaleDateString([], { weekday: 'short' });
|
||||
} else {
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmails() {
|
||||
// TODO: Load real emails from server
|
||||
// For now, generate mock data
|
||||
this.generateMockEmails();
|
||||
}
|
||||
|
||||
private async refreshEmails() {
|
||||
this.isLoading = true;
|
||||
await this.loadEmails();
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private async sendEmail(formData: any) {
|
||||
|
||||
try {
|
||||
// TODO: Implement actual email sending
|
||||
console.log('Sending email:', formData);
|
||||
|
||||
// Add to sent folder
|
||||
const newEmail: IEmail = {
|
||||
id: `email-${Date.now()}`,
|
||||
from: 'me@dcrouter.local',
|
||||
to: formData.to,
|
||||
cc: formData.cc || [],
|
||||
bcc: formData.bcc || [],
|
||||
subject: formData.subject,
|
||||
body: formData.body,
|
||||
date: Date.now(),
|
||||
read: true,
|
||||
folder: 'sent',
|
||||
};
|
||||
|
||||
this.emails = [...this.emails, newEmail];
|
||||
this.showCompose = false;
|
||||
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.log('Email sent successfully');
|
||||
} catch (error: any) {
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.error('Failed to send email', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async markAsRead(emailId: string) {
|
||||
const email = this.emails.find(e => e.id === emailId);
|
||||
if (email) {
|
||||
email.read = true;
|
||||
this.emails = [...this.emails];
|
||||
}
|
||||
}
|
||||
|
||||
private async markAllAsRead() {
|
||||
this.emails = this.emails.map(e =>
|
||||
e.folder === this.selectedFolder ? { ...e, read: true } : e
|
||||
);
|
||||
}
|
||||
|
||||
private async deleteEmail(emailId: string) {
|
||||
const email = this.emails.find(e => e.id === emailId);
|
||||
if (email) {
|
||||
if (email.folder === 'trash') {
|
||||
// Permanently delete
|
||||
this.emails = this.emails.filter(e => e.id !== emailId);
|
||||
} else {
|
||||
// Move to trash
|
||||
email.folder = 'trash';
|
||||
this.emails = [...this.emails];
|
||||
}
|
||||
|
||||
if (this.selectedEmail?.id === emailId) {
|
||||
this.selectedEmail = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async replyToEmail(email: IEmail) {
|
||||
// TODO: Open compose with reply context
|
||||
this.showCompose = true;
|
||||
}
|
||||
|
||||
private async replyAllToEmail(email: IEmail) {
|
||||
// TODO: Open compose with reply all context
|
||||
this.showCompose = true;
|
||||
}
|
||||
|
||||
private async forwardEmail(email: IEmail) {
|
||||
// TODO: Open compose with forward context
|
||||
this.showCompose = true;
|
||||
}
|
||||
|
||||
private generateMockEmails() {
|
||||
const subjects = [
|
||||
'Server Alert: High CPU Usage',
|
||||
'Daily Report - Network Activity',
|
||||
'Security Update Required',
|
||||
'New User Registration',
|
||||
'Backup Completed Successfully',
|
||||
'DNS Query Spike Detected',
|
||||
'SSL Certificate Renewal Notice',
|
||||
'Monthly Usage Summary',
|
||||
];
|
||||
|
||||
const senders = [
|
||||
'monitoring@dcrouter.local',
|
||||
'alerts@system.local',
|
||||
'admin@company.com',
|
||||
'noreply@service.com',
|
||||
'support@vendor.com',
|
||||
];
|
||||
|
||||
const bodies = [
|
||||
'This is an automated alert regarding your server status.',
|
||||
'Please review the attached report for detailed information.',
|
||||
'Action required: Update your security settings.',
|
||||
'Your daily summary is ready for review.',
|
||||
'All systems are operating normally.',
|
||||
];
|
||||
|
||||
this.emails = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `email-${i}`,
|
||||
from: senders[Math.floor(Math.random() * senders.length)],
|
||||
to: ['admin@dcrouter.local'],
|
||||
subject: subjects[Math.floor(Math.random() * subjects.length)],
|
||||
body: bodies[Math.floor(Math.random() * bodies.length)],
|
||||
date: Date.now() - (i * 3600000), // 1 hour apart
|
||||
read: Math.random() > 0.3,
|
||||
folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash',
|
||||
attachments: Math.random() > 0.8 ? [{
|
||||
filename: 'report.pdf',
|
||||
size: 1024 * 1024,
|
||||
contentType: 'application/pdf',
|
||||
}] : undefined,
|
||||
}));
|
||||
}
|
||||
}
|
520
ts_web/elements/ops-view-network.ts
Normal file
520
ts_web/elements/ops-view-network.ts
Normal file
@ -0,0 +1,520 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||
import * as appstate from '../appstate.js';
|
||||
import { viewHostCss } from './shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-network': OpsViewNetwork;
|
||||
}
|
||||
}
|
||||
|
||||
interface INetworkRequest {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
method: string;
|
||||
url: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
protocol: 'http' | 'https' | 'tcp' | 'udp';
|
||||
statusCode?: number;
|
||||
duration: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
remoteIp: string;
|
||||
route?: string;
|
||||
}
|
||||
|
||||
@customElement('ops-view-network')
|
||||
export class OpsViewNetwork extends DeesElement {
|
||||
@state()
|
||||
private statsState = appstate.statsStatePart.getState();
|
||||
|
||||
@state()
|
||||
private selectedTimeRange: '1m' | '5m' | '15m' | '1h' | '24h' = '5m';
|
||||
|
||||
@state()
|
||||
private selectedProtocol: 'all' | 'http' | 'https' | 'smtp' | 'dns' = 'all';
|
||||
|
||||
@state()
|
||||
private networkRequests: INetworkRequest[] = [];
|
||||
|
||||
@state()
|
||||
private trafficData: Array<{ x: number; y: number }> = [];
|
||||
|
||||
@state()
|
||||
private isLoading = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.subscribeToStateParts();
|
||||
this.generateMockData(); // TODO: Replace with real data from metrics
|
||||
}
|
||||
|
||||
private subscribeToStateParts() {
|
||||
appstate.statsStatePart.state.subscribe((state) => {
|
||||
this.statsState = state;
|
||||
this.updateNetworkData();
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.networkContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.controlBar {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.controlGroup {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controlLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
dees-statsgrid {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chartSection {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.tableSection {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.protocolBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.protocolBadge.http {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.protocolBadge.https {
|
||||
background: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
.protocolBadge.tcp {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.protocolBadge.smtp {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.protocolBadge.dns {
|
||||
background: #e0f2f1;
|
||||
color: #00796b;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.statusBadge.success {
|
||||
background: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
.statusBadge.error {
|
||||
background: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.statusBadge.warning {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Network Activity</ops-sectionheading>
|
||||
|
||||
<div class="networkContainer">
|
||||
<!-- Control Bar -->
|
||||
<div class="controlBar">
|
||||
<div class="controlGroup">
|
||||
<span class="controlLabel">Time Range:</span>
|
||||
<dees-button-group>
|
||||
${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html`
|
||||
<dees-button
|
||||
@click=${() => this.selectedTimeRange = range}
|
||||
.type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'}
|
||||
>
|
||||
${range}
|
||||
</dees-button>
|
||||
`)}
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="controlGroup">
|
||||
<span class="controlLabel">Protocol:</span>
|
||||
<dees-input-dropdown
|
||||
.options=${[
|
||||
{ key: 'all', label: 'All Protocols' },
|
||||
{ key: 'http', label: 'HTTP' },
|
||||
{ key: 'https', label: 'HTTPS' },
|
||||
{ key: 'smtp', label: 'SMTP' },
|
||||
{ key: 'dns', label: 'DNS' },
|
||||
]}
|
||||
.selectedOption=${{ key: this.selectedProtocol, label: this.getProtocolLabel(this.selectedProtocol) }}
|
||||
@selectedOption=${(e: CustomEvent) => this.selectedProtocol = e.detail.key}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: auto;">
|
||||
<dees-button
|
||||
@click=${() => this.refreshData()}
|
||||
.disabled=${this.isLoading}
|
||||
>
|
||||
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
${this.renderNetworkStats()}
|
||||
|
||||
<!-- Traffic Chart -->
|
||||
<div class="chartSection">
|
||||
<dees-chart-area
|
||||
.label=${'Network Traffic'}
|
||||
.series=${[
|
||||
{
|
||||
name: 'Requests/min',
|
||||
data: this.trafficData,
|
||||
}
|
||||
]}
|
||||
></dees-chart-area>
|
||||
</div>
|
||||
|
||||
<!-- Requests Table -->
|
||||
<div class="tableSection">
|
||||
<dees-table
|
||||
.data=${this.getFilteredRequests()}
|
||||
.displayFunction=${(req: INetworkRequest) => ({
|
||||
Time: new Date(req.timestamp).toLocaleTimeString(),
|
||||
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
|
||||
Method: req.method,
|
||||
'Host:Port': `${req.hostname}:${req.port}`,
|
||||
Path: this.truncateUrl(req.url),
|
||||
Status: this.renderStatus(req.statusCode),
|
||||
Duration: `${req.duration}ms`,
|
||||
'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
|
||||
'Remote IP': req.remoteIp,
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'magnifyingGlass',
|
||||
type: ['inRow', 'doubleClick', 'contextmenu'],
|
||||
actionFunc: async (actionData) => {
|
||||
await this.showRequestDetails(actionData.item);
|
||||
}
|
||||
}
|
||||
]}
|
||||
heading1="Recent Network Activity"
|
||||
heading2="Last ${this.selectedTimeRange} of network requests"
|
||||
searchable
|
||||
.pagination=${true}
|
||||
.paginationSize=${50}
|
||||
dataName="request"
|
||||
></dees-table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async showRequestDetails(request: INetworkRequest) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'Request Details',
|
||||
content: html`
|
||||
<div style="padding: 20px;">
|
||||
<dees-dataview-codebox
|
||||
.heading=${'Request Information'}
|
||||
progLang="json"
|
||||
.codeToDisplay=${JSON.stringify({
|
||||
id: request.id,
|
||||
timestamp: new Date(request.timestamp).toISOString(),
|
||||
protocol: request.protocol,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
hostname: request.hostname,
|
||||
port: request.port,
|
||||
statusCode: request.statusCode,
|
||||
duration: `${request.duration}ms`,
|
||||
bytesIn: request.bytesIn,
|
||||
bytesOut: request.bytesOut,
|
||||
remoteIp: request.remoteIp,
|
||||
route: request.route,
|
||||
}, null, 2)}
|
||||
></dees-dataview-codebox>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Copy Request ID',
|
||||
iconName: 'copy',
|
||||
action: async () => {
|
||||
await navigator.clipboard.writeText(request.id);
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.log('Request ID copied to clipboard');
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private getFilteredRequests(): INetworkRequest[] {
|
||||
if (this.selectedProtocol === 'all') {
|
||||
return this.networkRequests;
|
||||
}
|
||||
|
||||
// Map protocol filter to actual protocol values
|
||||
const protocolMap: Record<string, string[]> = {
|
||||
'http': ['http'],
|
||||
'https': ['https'],
|
||||
'smtp': ['tcp'], // SMTP runs over TCP
|
||||
'dns': ['udp'], // DNS typically runs over UDP
|
||||
};
|
||||
|
||||
const allowedProtocols = protocolMap[this.selectedProtocol] || [this.selectedProtocol];
|
||||
return this.networkRequests.filter(req => allowedProtocols.includes(req.protocol));
|
||||
}
|
||||
|
||||
private renderStatus(statusCode?: number): TemplateResult {
|
||||
if (!statusCode) {
|
||||
return html`<span class="statusBadge warning">N/A</span>`;
|
||||
}
|
||||
|
||||
const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' :
|
||||
statusCode >= 400 ? 'error' : 'warning';
|
||||
|
||||
return html`<span class="statusBadge ${statusClass}">${statusCode}</span>`;
|
||||
}
|
||||
|
||||
private truncateUrl(url: string, maxLength = 50): string {
|
||||
if (url.length <= maxLength) return url;
|
||||
return url.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
private getProtocolLabel(protocol: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'all': 'All Protocols',
|
||||
'http': 'HTTP',
|
||||
'https': 'HTTPS',
|
||||
'smtp': 'SMTP',
|
||||
'dns': 'DNS',
|
||||
};
|
||||
return labels[protocol] || protocol.toUpperCase();
|
||||
}
|
||||
|
||||
private formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toFixed(0);
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private calculateRequestsPerSecond(): number {
|
||||
// TODO: Calculate from real data based on connection metrics
|
||||
// For now, return a calculated value based on active connections
|
||||
return Math.floor((this.statsState.serverStats?.activeConnections || 0) * 0.8);
|
||||
}
|
||||
|
||||
private calculateThroughput(): { in: number; out: number } {
|
||||
// TODO: Calculate from real connection data
|
||||
// For now, return estimated values
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
return {
|
||||
in: activeConnections * 1024 * 10, // 10KB per connection estimate
|
||||
out: activeConnections * 1024 * 50, // 50KB per connection estimate
|
||||
};
|
||||
}
|
||||
|
||||
private renderNetworkStats(): TemplateResult {
|
||||
const reqPerSec = this.calculateRequestsPerSecond();
|
||||
const throughput = this.calculateThroughput();
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
|
||||
// Generate trend data for requests per second
|
||||
const trendData = Array.from({ length: 20 }, (_, i) =>
|
||||
Math.max(0, reqPerSec + (Math.random() - 0.5) * 10)
|
||||
);
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'connections',
|
||||
title: 'Active Connections',
|
||||
value: activeConnections,
|
||||
type: 'number',
|
||||
icon: 'plug',
|
||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
|
||||
actions: [
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'magnifyingGlass',
|
||||
action: async () => {
|
||||
// TODO: Show connection details
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'requests',
|
||||
title: 'Requests/sec',
|
||||
value: reqPerSec,
|
||||
type: 'trend',
|
||||
icon: 'chartLine',
|
||||
color: '#3b82f6',
|
||||
trendData: trendData,
|
||||
description: `${this.formatNumber(reqPerSec)} req/s`,
|
||||
},
|
||||
{
|
||||
id: 'throughputIn',
|
||||
title: 'Throughput In',
|
||||
value: this.formatBytes(throughput.in),
|
||||
unit: '/s',
|
||||
type: 'number',
|
||||
icon: 'download',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
id: 'throughputOut',
|
||||
title: 'Throughput Out',
|
||||
value: this.formatBytes(throughput.out),
|
||||
unit: '/s',
|
||||
type: 'number',
|
||||
icon: 'upload',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Export Data',
|
||||
iconName: 'fileExport',
|
||||
action: async () => {
|
||||
// TODO: Export network data
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.log('Export feature coming soon');
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
private async refreshData() {
|
||||
this.isLoading = true;
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await this.updateNetworkData();
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private async updateNetworkData() {
|
||||
// TODO: Fetch real network data from the server
|
||||
// For now, using mock data
|
||||
this.generateMockData();
|
||||
}
|
||||
|
||||
private generateMockData() {
|
||||
// Generate mock network requests
|
||||
const now = Date.now();
|
||||
const protocols: Array<'http' | 'https' | 'tcp' | 'udp'> = ['http', 'https', 'tcp', 'udp'];
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'];
|
||||
const hosts = ['api.example.com', 'app.local', 'mail.server.com', 'dns.resolver.net'];
|
||||
|
||||
this.networkRequests = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: `req-${i}`,
|
||||
timestamp: now - (i * 5000), // 5 seconds apart
|
||||
method: methods[Math.floor(Math.random() * methods.length)],
|
||||
url: `/api/v1/resource/${Math.floor(Math.random() * 100)}`,
|
||||
hostname: hosts[Math.floor(Math.random() * hosts.length)],
|
||||
port: Math.random() > 0.5 ? 443 : 80,
|
||||
protocol: protocols[Math.floor(Math.random() * protocols.length)],
|
||||
statusCode: Math.random() > 0.8 ? 404 : 200,
|
||||
duration: Math.floor(Math.random() * 500),
|
||||
bytesIn: Math.floor(Math.random() * 10000),
|
||||
bytesOut: Math.floor(Math.random() * 50000),
|
||||
remoteIp: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
||||
route: 'main-route',
|
||||
}));
|
||||
|
||||
// Generate traffic data for chart
|
||||
this.trafficData = Array.from({ length: 60 }, (_, i) => ({
|
||||
x: now - (i * 60000), // 1 minute intervals
|
||||
y: Math.floor(Math.random() * 100) + 50,
|
||||
})).reverse();
|
||||
}
|
||||
}
|
@ -9,7 +9,9 @@ import {
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
@customElement('ops-view-overview')
|
||||
export class OpsViewOverview extends DeesElement {
|
||||
@ -38,39 +40,13 @@ export class OpsViewOverview extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.statCard h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
h2 {
|
||||
margin: 32px 0 16px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2196F3;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chartGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@ -92,6 +68,10 @@ export class OpsViewOverview extends DeesElement {
|
||||
color: #c00;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
dees-statsgrid {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement {
|
||||
Error loading statistics: ${this.statsState.error}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="statsGrid">
|
||||
${this.statsState.serverStats ? html`
|
||||
<div class="statCard">
|
||||
<h3>Server Status</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.uptime ? 'Online' : 'Offline'}</div>
|
||||
<div class="statLabel">Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}</div>
|
||||
</div>
|
||||
${this.renderServerStats()}
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Connections</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
|
||||
<div class="statLabel">Active connections</div>
|
||||
</div>
|
||||
${this.renderEmailStats()}
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Memory Usage</h3>
|
||||
<div class="statValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
|
||||
<div class="statLabel">of ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>CPU Usage</h3>
|
||||
<div class="statValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%</div>
|
||||
<div class="statLabel">Average load</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.statsState.emailStats ? html`
|
||||
<h2>Email Statistics</h2>
|
||||
<div class="statsGrid">
|
||||
<div class="statCard">
|
||||
<h3>Emails Sent</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.sent}</div>
|
||||
<div class="statLabel">Total sent</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Emails Received</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.received}</div>
|
||||
<div class="statLabel">Total received</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Failed Deliveries</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.failed}</div>
|
||||
<div class="statLabel">Delivery failures</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Queued</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.queued}</div>
|
||||
<div class="statLabel">In queue</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.dnsStats ? html`
|
||||
<h2>DNS Statistics</h2>
|
||||
<div class="statsGrid">
|
||||
<div class="statCard">
|
||||
<h3>DNS Queries</h3>
|
||||
<div class="statValue">${this.statsState.dnsStats.totalQueries}</div>
|
||||
<div class="statLabel">Total queries handled</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Cache Hit Rate</h3>
|
||||
<div class="statValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%</div>
|
||||
<div class="statLabel">Cache efficiency</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${this.renderDnsStats()}
|
||||
|
||||
<div class="chartGrid">
|
||||
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
||||
@ -222,4 +134,171 @@ export class OpsViewOverview extends DeesElement {
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private renderServerStats(): TemplateResult {
|
||||
if (!this.statsState.serverStats) return html``;
|
||||
|
||||
const cpuUsage = Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2);
|
||||
const memoryUsage = Math.round((this.statsState.serverStats.memoryUsage.heapUsed / this.statsState.serverStats.memoryUsage.heapTotal) * 100);
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Server Status',
|
||||
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
|
||||
type: 'text',
|
||||
icon: 'server',
|
||||
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
|
||||
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
title: 'Active Connections',
|
||||
value: this.statsState.serverStats.activeConnections,
|
||||
type: 'number',
|
||||
icon: 'networkWired',
|
||||
color: '#3b82f6',
|
||||
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
title: 'CPU Usage',
|
||||
value: cpuUsage,
|
||||
type: 'gauge',
|
||||
icon: 'microchip',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: '#22c55e' },
|
||||
{ value: 60, color: '#f59e0b' },
|
||||
{ value: 80, color: '#ef4444' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
title: 'Memory Usage',
|
||||
value: memoryUsage,
|
||||
type: 'percentage',
|
||||
icon: 'memory',
|
||||
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
|
||||
description: `${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}`,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'arrowsRotate',
|
||||
action: async () => {
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailStats(): TemplateResult {
|
||||
if (!this.statsState.emailStats) return html``;
|
||||
|
||||
const deliveryRate = this.statsState.emailStats.deliveryRate || 0;
|
||||
const bounceRate = this.statsState.emailStats.bounceRate || 0;
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'sent',
|
||||
title: 'Emails Sent',
|
||||
value: this.statsState.emailStats.sent,
|
||||
type: 'number',
|
||||
icon: 'paperPlane',
|
||||
color: '#22c55e',
|
||||
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
|
||||
},
|
||||
{
|
||||
id: 'received',
|
||||
title: 'Emails Received',
|
||||
value: this.statsState.emailStats.received,
|
||||
type: 'number',
|
||||
icon: 'envelope',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'queued',
|
||||
title: 'Queued',
|
||||
value: this.statsState.emailStats.queued,
|
||||
type: 'number',
|
||||
icon: 'clock',
|
||||
color: '#f59e0b',
|
||||
description: 'Pending delivery',
|
||||
},
|
||||
{
|
||||
id: 'failed',
|
||||
title: 'Failed',
|
||||
value: this.statsState.emailStats.failed,
|
||||
type: 'number',
|
||||
icon: 'triangleExclamation',
|
||||
color: '#ef4444',
|
||||
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<h2>Email Statistics</h2>
|
||||
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDnsStats(): TemplateResult {
|
||||
if (!this.statsState.dnsStats) return html``;
|
||||
|
||||
const cacheHitRate = Math.round(this.statsState.dnsStats.cacheHitRate * 100);
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'queries',
|
||||
title: 'DNS Queries',
|
||||
value: this.statsState.dnsStats.totalQueries,
|
||||
type: 'number',
|
||||
icon: 'globe',
|
||||
color: '#3b82f6',
|
||||
description: 'Total queries handled',
|
||||
},
|
||||
{
|
||||
id: 'cacheRate',
|
||||
title: 'Cache Hit Rate',
|
||||
value: cacheHitRate,
|
||||
type: 'percentage',
|
||||
icon: 'database',
|
||||
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
|
||||
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
|
||||
},
|
||||
{
|
||||
id: 'domains',
|
||||
title: 'Active Domains',
|
||||
value: this.statsState.dnsStats.activeDomains,
|
||||
type: 'number',
|
||||
icon: 'sitemap',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
id: 'responseTime',
|
||||
title: 'Avg Response Time',
|
||||
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
|
||||
unit: 'ms',
|
||||
type: 'number',
|
||||
icon: 'clockRotateLeft',
|
||||
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<h2>DNS Statistics</h2>
|
||||
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-stats')
|
||||
export class OpsViewStats extends DeesElement {
|
||||
@state()
|
||||
private statsState: appstate.IStatsState = {
|
||||
serverStats: null,
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
private uiState: appstate.IUiState = {
|
||||
activeView: 'dashboard',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const statsSubscription = appstate.statsStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((statsState) => {
|
||||
this.statsState = statsState;
|
||||
});
|
||||
this.rxSubscriptions.push(statsSubscription);
|
||||
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.refreshButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lastUpdated {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.statsSection {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metricsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metricCard {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.metricCard:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.metricLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.metricUnit {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.chartContainer {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Statistics</ops-sectionheading>
|
||||
|
||||
<div class="controls">
|
||||
<div class="refreshButton">
|
||||
<dees-button
|
||||
@click=${() => appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null)}
|
||||
.disabled=${this.statsState.isLoading}
|
||||
>
|
||||
${this.statsState.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
|
||||
</dees-button>
|
||||
<dees-button
|
||||
@click=${() => appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)}
|
||||
.type=${this.uiState.autoRefresh ? 'highlighted' : 'normal'}
|
||||
>
|
||||
Auto-refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
|
||||
</dees-button>
|
||||
</div>
|
||||
<div class="lastUpdated">
|
||||
${this.statsState.lastUpdated ? html`
|
||||
Last updated: ${new Date(this.statsState.lastUpdated).toLocaleTimeString()}
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.statsState.serverStats ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">Server Metrics</h2>
|
||||
<div class="metricsGrid">
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Uptime</div>
|
||||
<div class="metricValue">${this.formatUptime(this.statsState.serverStats.uptime)}</div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">CPU Usage</div>
|
||||
<div class="metricValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}<span class="metricUnit">%</span></div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Memory Used</div>
|
||||
<div class="metricValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Active Connections</div>
|
||||
<div class="metricValue">${this.statsState.serverStats.activeConnections}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chartContainer">
|
||||
<dees-chart-area
|
||||
.label=${'Server Performance (Last 24 Hours)'}
|
||||
.data=${[]}
|
||||
></dees-chart-area>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.emailStats ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">Email Statistics</h2>
|
||||
<dees-table
|
||||
.heading1=${'Email Metrics'}
|
||||
.heading2=${'Current statistics for email processing'}
|
||||
.data=${[
|
||||
{ metric: 'Total Sent', value: this.statsState.emailStats.sent, unit: 'emails' },
|
||||
{ metric: 'Total Received', value: this.statsState.emailStats.received, unit: 'emails' },
|
||||
{ metric: 'Failed Deliveries', value: this.statsState.emailStats.failed, unit: 'emails' },
|
||||
{ metric: 'Currently Queued', value: this.statsState.emailStats.queued, unit: 'emails' },
|
||||
{ metric: 'Average Delivery Time', value: this.statsState.emailStats.averageDeliveryTime, unit: 'ms' },
|
||||
{ metric: 'Delivery Rate', value: `${Math.round(this.statsState.emailStats.deliveryRate * 100)}`, unit: '%' },
|
||||
]}
|
||||
.displayFunction=${(item) => ({
|
||||
Metric: item.metric,
|
||||
Value: `${item.value} ${item.unit}`,
|
||||
})}
|
||||
></dees-table>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.dnsStats ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">DNS Statistics</h2>
|
||||
<div class="metricsGrid">
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Total Queries</div>
|
||||
<div class="metricValue">${this.formatNumber(this.statsState.dnsStats.totalQueries)}</div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Cache Hit Rate</div>
|
||||
<div class="metricValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}<span class="metricUnit">%</span></div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Average Response Time</div>
|
||||
<div class="metricValue">${this.statsState.dnsStats.averageResponseTime}<span class="metricUnit">ms</span></div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Domains Configured</div>
|
||||
<div class="metricValue">${this.statsState.dnsStats.activeDomains}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.securityMetrics ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">Security Metrics</h2>
|
||||
<dees-table
|
||||
.heading1=${'Security Events'}
|
||||
.heading2=${'Recent security-related activities'}
|
||||
.data=${[
|
||||
{ metric: 'Blocked IPs', value: this.statsState.securityMetrics.blockedIPs.length, severity: 'high' },
|
||||
{ metric: 'Failed Authentications', value: this.statsState.securityMetrics.authenticationFailures, severity: 'medium' },
|
||||
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'low' },
|
||||
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'medium' },
|
||||
{ metric: 'Malware Detected', value: this.statsState.securityMetrics.malwareDetected, severity: 'high' },
|
||||
{ metric: 'Phishing Detected', value: this.statsState.securityMetrics.phishingDetected, severity: 'high' },
|
||||
]}
|
||||
.displayFunction=${(item) => ({
|
||||
'Security Metric': item.metric,
|
||||
'Count': item.value,
|
||||
'Severity': item.severity,
|
||||
})}
|
||||
></dees-table>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
private formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`;
|
||||
} else if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user