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/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^2.3.1",
|
"@git.zone/tstest": "^2.3.1",
|
||||||
"@git.zone/tswatch": "^2.0.1",
|
"@git.zone/tswatch": "^2.0.1",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^24.0.0",
|
||||||
"node-forge": "^1.3.1"
|
"node-forge": "^1.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -29,7 +29,7 @@
|
|||||||
"@api.global/typedserver": "^3.0.74",
|
"@api.global/typedserver": "^3.0.74",
|
||||||
"@api.global/typedsocket": "^3.0.0",
|
"@api.global/typedsocket": "^3.0.0",
|
||||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
"@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",
|
"@design.estate/dees-element": "^2.0.42",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@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
|
specifier: ^6.4.1
|
||||||
version: 6.4.1
|
version: 6.4.1
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^1.8.0
|
specifier: ^1.8.1
|
||||||
version: 1.8.0
|
version: 1.8.1
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.0.42
|
specifier: ^2.0.42
|
||||||
version: 2.0.42
|
version: 2.0.42
|
||||||
@ -130,8 +130,8 @@ importers:
|
|||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.15.30
|
specifier: ^24.0.0
|
||||||
version: 22.15.30
|
version: 24.0.0
|
||||||
node-forge:
|
node-forge:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.1
|
version: 1.3.1
|
||||||
@ -344,8 +344,8 @@ packages:
|
|||||||
'@dabh/diagnostics@2.0.3':
|
'@dabh/diagnostics@2.0.3':
|
||||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@1.8.0':
|
'@design.estate/dees-catalog@1.8.1':
|
||||||
resolution: {integrity: sha512-8xUkp+fPjXR9/GM5fvc+eHogpirkcmW4Ao8vNLgIomejoLRamtIbICJQaVmpra20XJqPkOcnvHwUHyhLckwYJA==}
|
resolution: {integrity: sha512-luYrGMK5APzw7GgXSe4UpFE5oC3vwzKTTB0etH3o6Au1nxvllhDcWr6S5UkB+WL9lroslBcK8Icyhiq8kIxEHg==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.27':
|
'@design.estate/dees-comms@1.0.27':
|
||||||
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
||||||
@ -737,68 +737,68 @@ packages:
|
|||||||
'@mongodb-js/saslprep@1.2.2':
|
'@mongodb-js/saslprep@1.2.2':
|
||||||
resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==}
|
resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.70':
|
'@napi-rs/canvas-android-arm64@0.1.71':
|
||||||
resolution: {integrity: sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==}
|
resolution: {integrity: sha512-cxi3VCotIOS9kNFQI7dcysbVJi106pxryVY1Hi85pX+ZeqahRyeqc/NsLaZ998Ae99+F3HI5X/39G1Y/Byrf0A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.70':
|
'@napi-rs/canvas-darwin-arm64@0.1.71':
|
||||||
resolution: {integrity: sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==}
|
resolution: {integrity: sha512-7Y4D/6vIuMLYsVNtRM/w2j0+fB1GyqeOxc7I0BTx8eLP1S6BZE2Rj6zJfdG+zmLEOW0IlHa+VQq1q2MUAjW84w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.70':
|
'@napi-rs/canvas-darwin-x64@0.1.71':
|
||||||
resolution: {integrity: sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==}
|
resolution: {integrity: sha512-Z0IUqxclrYdfVt/SK9nKCzUHTOXKTWiygtO71YCzs0OtxKdNI7GJRJdYG48wXZEDQ/pqTF4F7Ifgtidfc2tYpg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
|
||||||
resolution: {integrity: sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==}
|
resolution: {integrity: sha512-KlpqqCASak5ruY+UIolJgmhMZ9Pa2o1QyaNu648L8sz4WNBbNa+aOT60XCLCL1VIKLv11B3MlNgiOHoYNmDhXQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.70':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.71':
|
||||||
resolution: {integrity: sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==}
|
resolution: {integrity: sha512-bdGZCGu8YQNAiu3nkIVVUp6nIn6fPd36IuZsLXTG027E52KyIuZ3obCxehSwjDIUNkFWvmff5D6JYfWwAoioEw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.71':
|
||||||
resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==}
|
resolution: {integrity: sha512-1R5sMWe9ur8uM+hAeylBwG0b6UHDR+iWQNgzXmF9vbBYRooQvmDWqpcgytKLJAC0vnWhIkKwqd7yExn7cwczmg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
|
||||||
resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==}
|
resolution: {integrity: sha512-xjjKsipueuG+LdKIk6/uAlqdo+rzGcmNpTZPXdakIT1sHX4NNSnQTzjRaj9Gh96Czjd9G89UWR0KIlE7fwOgFA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.71':
|
||||||
resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==}
|
resolution: {integrity: sha512-3s6YpklXDB4OeeULG1XTRyKrKAOo7c3HHEqM9A6N4STSjMaJtzmpp7tB/JTvAFeOeFte6gWN8IwC+7AjGJ6MpQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.70':
|
'@napi-rs/canvas-linux-x64-musl@0.1.71':
|
||||||
resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==}
|
resolution: {integrity: sha512-5v9aCLzCXw7u10ray5juQMdl7TykZSn1X5AIGYwBvTAcKSgrqaR9QkRxp1Lqk3njQmFekOW1SFN9bZ/i/6y6kA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.71':
|
||||||
resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==}
|
resolution: {integrity: sha512-oJughk6xjsRIr0Rd9EqjmZmhIMkvcPuXgr3MNn2QexTqn+YFOizrwHS5ha0BDfFl7TEGRvwaDUXBQtu8JKXb8A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.70':
|
'@napi-rs/canvas@0.1.71':
|
||||||
resolution: {integrity: sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==}
|
resolution: {integrity: sha512-92ybDocKl6JM48ZpYbj+A7Qt45IaTABDk0y3sDecEQfgdhfNzJtEityqNHoCZ4Vty2dldPkJhxgvOnbrQMXTTA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@oozcitak/dom@1.15.10':
|
'@oozcitak/dom@1.15.10':
|
||||||
@ -1661,8 +1661,8 @@ packages:
|
|||||||
'@types/node@18.19.111':
|
'@types/node@18.19.111':
|
||||||
resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==}
|
resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==}
|
||||||
|
|
||||||
'@types/node@22.15.30':
|
'@types/node@24.0.0':
|
||||||
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
|
resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==}
|
||||||
|
|
||||||
'@types/pidusage@2.0.5':
|
'@types/pidusage@2.0.5':
|
||||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
||||||
@ -2963,8 +2963,8 @@ packages:
|
|||||||
leac@0.6.0:
|
leac@0.6.0:
|
||||||
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||||
|
|
||||||
lenis@1.3.3:
|
lenis@1.3.4:
|
||||||
resolution: {integrity: sha512-DOopj/UKHS54E9l2g4BOpDUvsyvkd1zkv+ECtHxQ9Fto8ozzKSz7MccqT+KOyG0ABA/OHXZ7l9INx0peUoQ8rQ==}
|
resolution: {integrity: sha512-WIGk8wiV2ABm/T7M+NC+tAV8fjzNJD1J4z11aZ3mTtx7WAZX/4QdCNhBO0g/TqXISA+/3hTbzrPC4FW1nhoNMQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nuxt/kit': '>=3.0.0'
|
'@nuxt/kit': '>=3.0.0'
|
||||||
react: '>=17.0.0'
|
react: '>=17.0.0'
|
||||||
@ -4194,8 +4194,8 @@ packages:
|
|||||||
undici-types@5.26.5:
|
undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
undici-types@6.21.0:
|
undici-types@7.8.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
undici@7.10.0:
|
undici@7.10.0:
|
||||||
resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
|
resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
|
||||||
@ -5075,7 +5075,7 @@ snapshots:
|
|||||||
enabled: 2.0.0
|
enabled: 2.0.0
|
||||||
kuler: 2.0.0
|
kuler: 2.0.0
|
||||||
|
|
||||||
'@design.estate/dees-catalog@1.8.0':
|
'@design.estate/dees-catalog@1.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.2
|
'@design.estate/dees-domtools': 2.3.2
|
||||||
'@design.estate/dees-element': 2.0.42
|
'@design.estate/dees-element': 2.0.42
|
||||||
@ -5127,7 +5127,7 @@ snapshots:
|
|||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 3.0.37
|
||||||
'@push.rocks/websetup': 3.0.19
|
'@push.rocks/websetup': 3.0.19
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
lenis: 1.3.3
|
lenis: 1.3.4
|
||||||
lit: 3.3.0
|
lit: 3.3.0
|
||||||
sweet-scroll: 4.0.0
|
sweet-scroll: 4.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -5487,48 +5487,48 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.70':
|
'@napi-rs/canvas-android-arm64@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.70':
|
'@napi-rs/canvas-darwin-arm64@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.70':
|
'@napi-rs/canvas-darwin-x64@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.70':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.70':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.70':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.70':
|
'@napi-rs/canvas-linux-x64-musl@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.70':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.71':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.70':
|
'@napi-rs/canvas@0.1.71':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas-android-arm64': 0.1.70
|
'@napi-rs/canvas-android-arm64': 0.1.71
|
||||||
'@napi-rs/canvas-darwin-arm64': 0.1.70
|
'@napi-rs/canvas-darwin-arm64': 0.1.71
|
||||||
'@napi-rs/canvas-darwin-x64': 0.1.70
|
'@napi-rs/canvas-darwin-x64': 0.1.71
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.70
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.71
|
||||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.70
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.71
|
||||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.70
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.71
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.70
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.71
|
||||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.70
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.71
|
||||||
'@napi-rs/canvas-linux-x64-musl': 0.1.70
|
'@napi-rs/canvas-linux-x64-musl': 0.1.71
|
||||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.70
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.71
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oozcitak/dom@1.15.10':
|
'@oozcitak/dom@1.15.10':
|
||||||
@ -5784,7 +5784,6 @@ snapshots:
|
|||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- bufferutil
|
|
||||||
- encoding
|
- encoding
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
@ -5793,7 +5792,6 @@ snapshots:
|
|||||||
- snappy
|
- snappy
|
||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartarchive@3.0.8':
|
'@push.rocks/smartarchive@3.0.8':
|
||||||
@ -7046,27 +7044,27 @@ snapshots:
|
|||||||
|
|
||||||
'@types/bn.js@5.2.0':
|
'@types/bn.js@5.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/body-parser@1.19.5':
|
'@types/body-parser@1.19.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/buffer-json@2.0.3': {}
|
'@types/buffer-json@2.0.3': {}
|
||||||
|
|
||||||
'@types/clean-css@4.2.11':
|
'@types/clean-css@4.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/cors@2.8.18':
|
'@types/cors@2.8.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7078,7 +7076,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/dns-packet@5.6.5':
|
'@types/dns-packet@5.6.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/elliptic@6.4.18':
|
'@types/elliptic@6.4.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7086,7 +7084,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@5.0.6':
|
'@types/express-serve-static-core@5.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
@ -7103,30 +7101,30 @@ snapshots:
|
|||||||
|
|
||||||
'@types/from2@2.3.5':
|
'@types/from2@2.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/fs-extra@11.0.4':
|
'@types/fs-extra@11.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jsonfile': 6.1.4
|
'@types/jsonfile': 6.1.4
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/glob@7.2.0':
|
'@types/glob@7.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/glob@8.1.0':
|
'@types/glob@8.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/gunzip-maybe@1.4.2':
|
'@types/gunzip-maybe@1.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7148,16 +7146,16 @@ snapshots:
|
|||||||
|
|
||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.9':
|
'@types/jsonwebtoken@9.0.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/mailparser@3.4.6':
|
'@types/mailparser@3.4.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
@ -7176,20 +7174,20 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
form-data: 4.0.2
|
form-data: 4.0.2
|
||||||
|
|
||||||
'@types/node-forge@1.3.11':
|
'@types/node-forge@1.3.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/node@18.19.111':
|
'@types/node@18.19.111':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
||||||
'@types/node@22.15.30':
|
'@types/node@24.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 7.8.0
|
||||||
|
|
||||||
'@types/pidusage@2.0.5': {}
|
'@types/pidusage@2.0.5': {}
|
||||||
|
|
||||||
@ -7205,30 +7203,30 @@ snapshots:
|
|||||||
|
|
||||||
'@types/s3rver@3.7.4':
|
'@types/s3rver@3.7.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/semver@7.7.0': {}
|
'@types/semver@7.7.0': {}
|
||||||
|
|
||||||
'@types/send@0.17.4':
|
'@types/send@0.17.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime': 1.3.5
|
'@types/mime': 1.3.5
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/serve-static@1.15.7':
|
'@types/serve-static@1.15.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.4
|
'@types/http-errors': 2.0.4
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
|
|
||||||
'@types/symbol-tree@3.2.5': {}
|
'@types/symbol-tree@3.2.5': {}
|
||||||
|
|
||||||
'@types/tar-stream@2.2.3':
|
'@types/tar-stream@2.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/through2@2.0.41':
|
'@types/through2@2.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
@ -7252,18 +7250,18 @@ snapshots:
|
|||||||
|
|
||||||
'@types/whatwg-url@8.2.2':
|
'@types/whatwg-url@8.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
'@types/webidl-conversions': 7.0.3
|
'@types/webidl-conversions': 7.0.3
|
||||||
|
|
||||||
'@types/which@3.0.4': {}
|
'@types/which@3.0.4': {}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@ -7835,7 +7833,7 @@ snapshots:
|
|||||||
engine.io@6.6.4:
|
engine.io@6.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cors': 2.8.18
|
'@types/cors': 2.8.18
|
||||||
'@types/node': 22.15.30
|
'@types/node': 24.0.0
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
@ -8663,7 +8661,7 @@ snapshots:
|
|||||||
|
|
||||||
leac@0.6.0: {}
|
leac@0.6.0: {}
|
||||||
|
|
||||||
lenis@1.3.3: {}
|
lenis@1.3.4: {}
|
||||||
|
|
||||||
libbase64@1.3.0: {}
|
libbase64@1.3.0: {}
|
||||||
|
|
||||||
@ -9473,7 +9471,7 @@ snapshots:
|
|||||||
|
|
||||||
pdfjs-dist@4.10.38:
|
pdfjs-dist@4.10.38:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.70
|
'@napi-rs/canvas': 0.1.71
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
@ -10168,7 +10166,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@5.26.5: {}
|
undici-types@5.26.5: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
undici@7.10.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-dashboard.js';
|
||||||
export * from './ops-view-overview.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-logs.js';
|
||||||
export * from './ops-view-config.js';
|
export * from './ops-view-config.js';
|
||||||
export * from './ops-view-security.js';
|
export * from './ops-view-security.js';
|
||||||
|
@ -13,7 +13,8 @@ import {
|
|||||||
|
|
||||||
// Import view components
|
// Import view components
|
||||||
import { OpsViewOverview } from './ops-view-overview.js';
|
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 { OpsViewLogs } from './ops-view-logs.js';
|
||||||
import { OpsViewConfig } from './ops-view-config.js';
|
import { OpsViewConfig } from './ops-view-config.js';
|
||||||
import { OpsViewSecurity } from './ops-view-security.js';
|
import { OpsViewSecurity } from './ops-view-security.js';
|
||||||
@ -84,8 +85,12 @@ export class OpsDashboard extends DeesElement {
|
|||||||
element: OpsViewOverview,
|
element: OpsViewOverview,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Statistics',
|
name: 'Network',
|
||||||
element: OpsViewStats,
|
element: OpsViewNetwork,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Emails',
|
||||||
|
element: OpsViewEmails,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logs',
|
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,
|
state,
|
||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
@customElement('ops-view-overview')
|
@customElement('ops-view-overview')
|
||||||
export class OpsViewOverview extends DeesElement {
|
export class OpsViewOverview extends DeesElement {
|
||||||
@ -38,39 +40,13 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.statsGrid {
|
h2 {
|
||||||
display: grid;
|
margin: 32px 0 16px 0;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
font-size: 24px;
|
||||||
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;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statValue {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2196F3;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statLabel {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chartGrid {
|
.chartGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
@ -92,6 +68,10 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
color: #c00;
|
color: #c00;
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dees-statsgrid {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
Error loading statistics: ${this.statsState.error}
|
Error loading statistics: ${this.statsState.error}
|
||||||
</div>
|
</div>
|
||||||
` : html`
|
` : html`
|
||||||
<div class="statsGrid">
|
${this.renderServerStats()}
|
||||||
${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>
|
|
||||||
|
|
||||||
<div class="statCard">
|
|
||||||
<h3>Connections</h3>
|
|
||||||
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
|
|
||||||
<div class="statLabel">Active connections</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="statCard">
|
${this.renderEmailStats()}
|
||||||
<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">
|
${this.renderDnsStats()}
|
||||||
<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>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div class="chartGrid">
|
<div class="chartGrid">
|
||||||
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
<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]}`;
|
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