From 1238dbe5f00212981da6e9ea74e216068f919377 Mon Sep 17 00:00:00 2001 From: shm Date: Mon, 23 Dec 2024 23:38:10 +0900 Subject: [PATCH 1/6] Add: Get Network Info --- src-tauri/src/commands/hardware.rs | 11 ++++++ src-tauri/src/enums/error.rs | 32 +++++++++++++++++ src-tauri/src/enums/mod.rs | 1 + src-tauri/src/lib.rs | 1 + src-tauri/src/services/system_info_service.rs | 36 ++++++++++++++++++- src-tauri/src/structs/hardware.rs | 18 ++++++++++ src/atom/useHardwareInfoAtom.ts | 18 ++++++++-- src/rspc/bindings.ts | 14 ++++++++ src/template/Dashboard.tsx | 32 +++++++++++++++++ 9 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/enums/error.rs diff --git a/src-tauri/src/commands/hardware.rs b/src-tauri/src/commands/hardware.rs index b278f1c..6c90554 100644 --- a/src-tauri/src/commands/hardware.rs +++ b/src-tauri/src/commands/hardware.rs @@ -1,7 +1,9 @@ +use crate::enums::error::BackendError; use crate::services::directx_gpu_service; use crate::services::nvidia_gpu_service; use crate::services::system_info_service; use crate::services::wmi_service; +use crate::structs::hardware::NetworkInfo; use crate::structs::hardware::{GraphicInfo, MemoryInfo, StorageInfo}; use crate::{log_error, log_internal}; use serde::{Deserialize, Serialize, Serializer}; @@ -314,6 +316,15 @@ pub fn get_gpu_usage_history( .collect() } +/// +/// ## ネットワーク情報を取得 +/// +#[command] +#[specta::specta] +pub fn get_network_info() -> Result, BackendError> { + system_info_service::get_network_info() +} + /// /// ## システム情報の初期化 /// diff --git a/src-tauri/src/enums/error.rs b/src-tauri/src/enums/error.rs new file mode 100644 index 0000000..2c9286b --- /dev/null +++ b/src-tauri/src/enums/error.rs @@ -0,0 +1,32 @@ +use serde::{Serialize, Serializer}; +use specta::Type; + +#[derive(Debug, PartialEq, Eq, Clone, Type)] +#[serde(rename_all = "camelCase")] +pub enum BackendError { + CpuInfoNotAvailable, + StorageInfoNotAvailable, + MemoryInfoNotAvailable, + GraphicInfoNotAvailable, + NetworkInfoNotAvailable, + NetworkUsageNotAvailable, + // SystemError(String), +} + +impl Serialize for BackendError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = match *self { + BackendError::CpuInfoNotAvailable => "cpuInfoNotAvailable", + BackendError::StorageInfoNotAvailable => "storageInfoNotAvailable", + BackendError::MemoryInfoNotAvailable => "memoryInfoNotAvailable", + BackendError::GraphicInfoNotAvailable => "graphicInfoNotAvailable", + BackendError::NetworkInfoNotAvailable => "networkInfoNotAvailable", + BackendError::NetworkUsageNotAvailable => "networkUsageNotAvailable", + // BackendError::SystemError(ref e) => e, + }; + serializer.serialize_str(s) + } +} diff --git a/src-tauri/src/enums/mod.rs b/src-tauri/src/enums/mod.rs index 2acb6bb..103a734 100644 --- a/src-tauri/src/enums/mod.rs +++ b/src-tauri/src/enums/mod.rs @@ -1,2 +1,3 @@ pub mod config; +pub mod error; pub mod hardware; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3f3e729..0ec9859 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -66,6 +66,7 @@ pub fn run() { hardware::get_cpu_usage_history, hardware::get_memory_usage_history, hardware::get_gpu_usage_history, + hardware::get_network_info, config::commands::get_settings, config::commands::set_language, config::commands::set_theme, diff --git a/src-tauri/src/services/system_info_service.rs b/src-tauri/src/services/system_info_service.rs index 319be22..178c80e 100644 --- a/src-tauri/src/services/system_info_service.rs +++ b/src-tauri/src/services/system_info_service.rs @@ -1,12 +1,14 @@ use crate::enums; +use crate::enums::error::BackendError; use crate::structs; +use crate::structs::hardware::NetworkInfo; use crate::utils; use crate::utils::formatter::SizeUnit; use serde::{Deserialize, Serialize}; use specta::Type; use std::sync::MutexGuard; -use sysinfo::{Disks, System}; +use sysinfo::{Disks, IpNetwork, NetworkData, Networks, System}; #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] @@ -73,3 +75,35 @@ pub fn get_storage_info() -> Result, String> Ok(storage_info) } + +pub fn get_network_info() -> Result, BackendError> { + let interfaces = Networks::new_with_refreshed_list(); + + if interfaces.is_empty() { + return Err(BackendError::NetworkInfoNotAvailable); + } + + let network_info = interfaces + .values() + .map(|interface| { + let ip_networks = interface.ip_networks(); + let mac = interface.mac_address(); + + // IPv4とIPv6を分ける + let (ipv4, ipv6): (Vec<_>, Vec<_>) = ip_networks + .iter() + .partition(|ip_network| ip_network.addr.is_ipv4()); + + Ok(NetworkInfo { + ipv4: ipv4 + .into_iter() + .map(|ip: &IpNetwork| ip.addr.to_string()) + .collect(), + ipv6: ipv6.into_iter().map(|ip| ip.addr.to_string()).collect(), + mac: mac.to_string(), + }) + }) + .collect::, _>>()?; + + Ok(network_info) +} diff --git a/src-tauri/src/structs/hardware.rs b/src-tauri/src/structs/hardware.rs index d5853e9..18da172 100644 --- a/src-tauri/src/structs/hardware.rs +++ b/src-tauri/src/structs/hardware.rs @@ -35,3 +35,21 @@ pub struct StorageInfo { pub storage_type: DiskKind, pub file_system: String, } + +#[derive(Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct NetworkInfo { + pub ipv4: Vec, + pub ipv6: Vec, + pub mac: String, +} + +#[derive(Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct NetworkUsage { + pub ip: String, + pub sent: f32, + pub sent_unit: SizeUnit, + pub received: f32, + pub received_unit: SizeUnit, +} diff --git a/src/atom/useHardwareInfoAtom.ts b/src/atom/useHardwareInfoAtom.ts index b9468f1..51bbf50 100644 --- a/src/atom/useHardwareInfoAtom.ts +++ b/src/atom/useHardwareInfoAtom.ts @@ -1,5 +1,5 @@ import { useTauriDialog } from "@/hooks/useTauriDialog"; -import { type SysInfo, commands } from "@/rspc/bindings"; +import { type NetworkInfo, type SysInfo, commands } from "@/rspc/bindings"; import { isError } from "@/types/result"; import { atom, useAtom } from "jotai"; @@ -10,8 +10,11 @@ const hardInfoAtom = atom({ storage: [], }); +const networkInfoAtom = atom([]); + export const useHardwareInfoAtom = () => { const [hardwareInfo, setHardInfo] = useAtom(hardInfoAtom); + const [networkInfo, setNetworkInfo] = useAtom(networkInfoAtom); const { error } = useTauriDialog(); const init = async () => { @@ -25,5 +28,16 @@ export const useHardwareInfoAtom = () => { setHardInfo(fetchedHardwareInfo.data); }; - return { hardwareInfo, init }; + const initNetwork = async () => { + const fetchedNetworkInfo = await commands.getNetworkInfo(); + if (isError(fetchedNetworkInfo)) { + error(fetchedNetworkInfo.error); + console.error("Failed to fetch network info:", fetchedNetworkInfo); + return; + } + + setNetworkInfo(fetchedNetworkInfo.data); + }; + + return { hardwareInfo, networkInfo, init, initNetwork }; }; diff --git a/src/rspc/bindings.ts b/src/rspc/bindings.ts index defff9b..738e3ff 100644 --- a/src/rspc/bindings.ts +++ b/src/rspc/bindings.ts @@ -114,6 +114,18 @@ async getMemoryUsageHistory(seconds: number) : Promise { async getGpuUsageHistory(seconds: number) : Promise { return await TAURI_INVOKE("get_gpu_usage_history", { seconds }); }, +/** + * ## ネットワーク情報を取得 + * + */ +async getNetworkInfo() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_network_info") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async getSettings() : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_settings") }; @@ -292,6 +304,7 @@ async setDecoration(isDecorated: boolean) : Promise { /** user-defined types **/ +export type BackendError = "cpuInfoNotAvailable" | "storageInfoNotAvailable" | "memoryInfoNotAvailable" | "graphicInfoNotAvailable" | "networkInfoNotAvailable" | "networkUsageNotAvailable" /** * - `file_id` : 画像ファイルID * - `image_data` : 画像データのBase64文字列 @@ -311,6 +324,7 @@ export type HardwareType = "cpu" | "memory" | "gpu" export type LineGraphColorStringSettings = { cpu: string; memory: string; gpu: string } export type MemoryInfo = { size: string; clock: number; clockUnit: string; memoryCount: number; totalSlots: number; memoryType: string } export type NameValue = { name: string; value: number } +export type NetworkInfo = { ipv4: string[]; ipv6: string[]; mac: string } export type ProcessInfo = { pid: number; name: string; cpuUsage: number; memoryUsage: number } export type SizeUnit = "B" | "KB" | "MB" | "GB" export type StorageInfo = { name: string; size: number; sizeUnit: SizeUnit; free: number; freeUnit: SizeUnit; storageType: DiskKind; fileSystem: string } diff --git a/src/template/Dashboard.tsx b/src/template/Dashboard.tsx index f68e0e9..3fe29f6 100644 --- a/src/template/Dashboard.tsx +++ b/src/template/Dashboard.tsx @@ -225,6 +225,34 @@ const StorageDataInfo = () => { ); }; +const NetworkInfo = () => { + const { t } = useTranslation(); + const { networkInfo, initNetwork } = useHardwareInfoAtom(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + initNetwork(); + }, []); + + return ( + <> + {networkInfo.map((network) => { + return ( +
+ +
+ ); + })} + + ); +}; + const Dashboard = () => { const { hardwareInfo } = useHardwareInfoAtom(); const { init } = useHardwareInfoAtom(); @@ -252,6 +280,10 @@ const Dashboard = () => { key: "processesTable", component: , }, + { + key: "networkInfo", + component: , + }, ].filter((x) => x != null); const dataAreaKey2Title: Record = { From 3852c2696361af3657920a49bb0e3d4e5d1c70a2 Mon Sep 17 00:00:00 2001 From: shm Date: Tue, 31 Dec 2024 14:43:44 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=E3=83=8D=E3=83=83=E3=83=88=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E6=83=85=E5=A0=B1=E3=81=AE=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=81=AE=E6=94=B9=E8=89=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/hardware.rs | 2 +- src-tauri/src/enums/error.rs | 2 + src-tauri/src/services/system_info_service.rs | 36 +------- src-tauri/src/services/wmi_service.rs | 90 ++++++++++++++++++- src-tauri/src/structs/hardware.rs | 11 ++- src-tauri/src/tests/utils/ip.rs | 53 +++++++++++ src-tauri/src/tests/utils/mod.rs | 2 + src-tauri/src/utils/ip.rs | 11 +++ src-tauri/src/utils/mod.rs | 1 + src/rspc/bindings.ts | 4 +- src/template/Dashboard.tsx | 13 ++- 11 files changed, 178 insertions(+), 47 deletions(-) create mode 100644 src-tauri/src/tests/utils/ip.rs create mode 100644 src-tauri/src/utils/ip.rs diff --git a/src-tauri/src/commands/hardware.rs b/src-tauri/src/commands/hardware.rs index 6c90554..24409e1 100644 --- a/src-tauri/src/commands/hardware.rs +++ b/src-tauri/src/commands/hardware.rs @@ -322,7 +322,7 @@ pub fn get_gpu_usage_history( #[command] #[specta::specta] pub fn get_network_info() -> Result, BackendError> { - system_info_service::get_network_info() + wmi_service::get_network_info().map_err(|_| BackendError::UnexpectedError) } /// diff --git a/src-tauri/src/enums/error.rs b/src-tauri/src/enums/error.rs index 2c9286b..b9048a4 100644 --- a/src-tauri/src/enums/error.rs +++ b/src-tauri/src/enums/error.rs @@ -10,6 +10,7 @@ pub enum BackendError { GraphicInfoNotAvailable, NetworkInfoNotAvailable, NetworkUsageNotAvailable, + UnexpectedError, // SystemError(String), } @@ -25,6 +26,7 @@ impl Serialize for BackendError { BackendError::GraphicInfoNotAvailable => "graphicInfoNotAvailable", BackendError::NetworkInfoNotAvailable => "networkInfoNotAvailable", BackendError::NetworkUsageNotAvailable => "networkUsageNotAvailable", + BackendError::UnexpectedError => "unexpectedError", // BackendError::SystemError(ref e) => e, }; serializer.serialize_str(s) diff --git a/src-tauri/src/services/system_info_service.rs b/src-tauri/src/services/system_info_service.rs index 178c80e..319be22 100644 --- a/src-tauri/src/services/system_info_service.rs +++ b/src-tauri/src/services/system_info_service.rs @@ -1,14 +1,12 @@ use crate::enums; -use crate::enums::error::BackendError; use crate::structs; -use crate::structs::hardware::NetworkInfo; use crate::utils; use crate::utils::formatter::SizeUnit; use serde::{Deserialize, Serialize}; use specta::Type; use std::sync::MutexGuard; -use sysinfo::{Disks, IpNetwork, NetworkData, Networks, System}; +use sysinfo::{Disks, System}; #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] @@ -75,35 +73,3 @@ pub fn get_storage_info() -> Result, String> Ok(storage_info) } - -pub fn get_network_info() -> Result, BackendError> { - let interfaces = Networks::new_with_refreshed_list(); - - if interfaces.is_empty() { - return Err(BackendError::NetworkInfoNotAvailable); - } - - let network_info = interfaces - .values() - .map(|interface| { - let ip_networks = interface.ip_networks(); - let mac = interface.mac_address(); - - // IPv4とIPv6を分ける - let (ipv4, ipv6): (Vec<_>, Vec<_>) = ip_networks - .iter() - .partition(|ip_network| ip_network.addr.is_ipv4()); - - Ok(NetworkInfo { - ipv4: ipv4 - .into_iter() - .map(|ip: &IpNetwork| ip.addr.to_string()) - .collect(), - ipv6: ipv6.into_iter().map(|ip| ip.addr.to_string()).collect(), - mac: mac.to_string(), - }) - }) - .collect::, _>>()?; - - Ok(network_info) -} diff --git a/src-tauri/src/services/wmi_service.rs b/src-tauri/src/services/wmi_service.rs index cc2fdb6..e1f1697 100644 --- a/src-tauri/src/services/wmi_service.rs +++ b/src-tauri/src/services/wmi_service.rs @@ -1,4 +1,5 @@ -use crate::structs::hardware::MemoryInfo; +use crate::structs::hardware::{MemoryInfo, NetworkInfo}; +use crate::utils; use crate::utils::formatter; use crate::{log_error, log_info, log_internal}; @@ -6,6 +7,7 @@ use regex::Regex; use serde::de::DeserializeOwned; use serde::Deserialize; use std::error::Error; +use std::net::IpAddr; use std::sync::mpsc::{channel, Receiver, Sender}; use std::thread; use wmi::{COMLibrary, WMIConnection}; @@ -105,8 +107,92 @@ pub async fn get_gpu_usage_by_device_and_engine( }) } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +struct NetworkAdapterConfiguration { + description: Option, + #[serde(rename = "MACAddress")] + mac_address: Option, + #[serde(rename = "IPAddress")] + ip_address: Option>, + #[serde(rename = "IPSubnet")] + ip_subnet: Option>, + #[serde(rename = "DefaultIPGateway")] + default_ip_gateway: Option>, +} + +pub fn get_network_info() -> Result, String> { + let results: Vec = wmi_query_in_thread( + "SELECT Description, MACAddress, IPAddress, IPSubnet, DefaultIPGateway FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = TRUE".to_string(), + )?; + + log_info!( + &format!("Network adapter configuration data: {:?}", results), + "get_network_info", + None::<&str> + ); + + let network_info: Result, String> = results + .into_iter() + .map(|adapter| -> Result { + // IPv4とIPv6を分ける + let (ipv4, ipv6): (Vec<_>, Vec<_>) = adapter + .ip_address + .unwrap_or_else(|| vec![]) + .into_iter() + .filter_map(|ip| ip.parse::().ok()) + .partition(|ip| matches!(ip, IpAddr::V4(_))); // IPv4とIPv6に分ける + + // IPv6を分割する + let (link_local_ipv6, normal_ipv6): (Vec<_>, Vec<_>) = + ipv6.into_iter().partition(|ip| match ip { + IpAddr::V6(v6) if utils::ip::is_unicast_link_local(v6) => true, // リンクローカル + _ => false, + }); + + // IPv4サブネットを取得 + let ipv4_subnet: Vec = adapter + .ip_subnet + .unwrap_or_else(|| vec![]) + .into_iter() + .filter(|subnet| subnet.contains('.')) // IPv4形式を確認 + .collect(); + + // IPv4とIPv6のデフォルトゲートウェイを分割する + let (default_ipv4_gateway, default_ipv6_gateway): (Vec<_>, Vec<_>) = adapter + .default_ip_gateway + .unwrap_or_else(|| vec![]) + .into_iter() + .filter_map(|ip| ip.parse::().ok()) + .partition(|ip| matches!(ip, IpAddr::V4(_))); + + Ok(NetworkInfo { + description: Some(adapter.description.unwrap_or_default()), + mac_address: Some(adapter.mac_address.unwrap_or_default()), + ip_v4: ipv4.into_iter().map(|ip| ip.to_string()).collect(), + ip_v6: normal_ipv6.into_iter().map(|ip| ip.to_string()).collect(), + link_local_ip_v6: link_local_ipv6 + .into_iter() + .map(|ip| ip.to_string()) + .collect(), + ip_subnet: ipv4_subnet, + default_ipv4_gateway: default_ipv4_gateway + .into_iter() + .map(|ip| ip.to_string()) + .collect(), + default_ipv6_gateway: default_ipv6_gateway + .into_iter() + .map(|ip| ip.to_string()) + .collect(), + }) + }) + .collect(); + + network_info +} + /// -/// ## WMIから別スレッドで¥クエリ実行する(WMIを使用) +/// ## 別スレッドでWMIクエリ実行する /// fn wmi_query_in_thread(query: String) -> Result, String> where diff --git a/src-tauri/src/structs/hardware.rs b/src-tauri/src/structs/hardware.rs index 18da172..7fd4d3e 100644 --- a/src-tauri/src/structs/hardware.rs +++ b/src-tauri/src/structs/hardware.rs @@ -39,9 +39,14 @@ pub struct StorageInfo { #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct NetworkInfo { - pub ipv4: Vec, - pub ipv6: Vec, - pub mac: String, + pub description: Option, + pub mac_address: Option, + pub ip_v4: Vec, + pub ip_v6: Vec, + pub link_local_ip_v6: Vec, + pub ip_subnet: Vec, + pub default_ipv4_gateway: Vec, + pub default_ipv6_gateway: Vec, } #[derive(Serialize, Deserialize, Type)] diff --git a/src-tauri/src/tests/utils/ip.rs b/src-tauri/src/tests/utils/ip.rs new file mode 100644 index 0000000..f274b1b --- /dev/null +++ b/src-tauri/src/tests/utils/ip.rs @@ -0,0 +1,53 @@ +#[cfg(test)] +mod tests { + use crate::utils::ip::*; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + #[test] + fn test_link_local_ipv6() { + // リンクローカルアドレス + let ip = IpAddr::V6("fe80::1".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), true); + + // グローバルIPv6アドレス + let ip = IpAddr::V6("2400:4051::1".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), true); + + // IPv6マルチキャスト + let ip = IpAddr::V6("ff02::1".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), false); + + // 未指定アドレス + let ip = IpAddr::V6("::".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), false); + } + + #[test] + fn test_ipv4_not_link_local() { + // IPv4アドレス(リンクローカルではない) + let ip = IpAddr::V4("192.168.1.1".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), false); + + // IPv4アドレス(リンクローカルではない) + let ip = IpAddr::V4("127.0.0.1".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), false); + } + + #[test] + fn test_with_direct_ipv6() { + // 直接Ipv6Addr型でテスト + let ip = "fe80::1".parse::().unwrap(); + assert_eq!(is_unicast_link_local(&ip), true); + + let ip = "2400:4051::1".parse::().unwrap(); + assert_eq!(is_unicast_link_local(&ip), false); + } + + #[test] + fn test_with_invalid_format() { + // 無効なアドレス文字列はパースエラーを引き起こすためここではテストしないが、 + // 型制約に沿ってテストする例 + let ip = IpAddr::V4("255.255.255.255".parse::().unwrap()); + assert_eq!(is_unicast_link_local(&ip), false); + } +} diff --git a/src-tauri/src/tests/utils/mod.rs b/src-tauri/src/tests/utils/mod.rs index 8591225..ce5adcb 100644 --- a/src-tauri/src/tests/utils/mod.rs +++ b/src-tauri/src/tests/utils/mod.rs @@ -2,3 +2,5 @@ pub mod color; #[cfg(test)] pub mod formatter; +#[cfg(test)] +pub mod ip; diff --git a/src-tauri/src/utils/ip.rs b/src-tauri/src/utils/ip.rs new file mode 100644 index 0000000..a84043d --- /dev/null +++ b/src-tauri/src/utils/ip.rs @@ -0,0 +1,11 @@ +use std::net::IpAddr; + +pub fn is_unicast_link_local(ip: &T) -> bool +where + T: Into + Clone, +{ + match ip.clone().into() { + IpAddr::V6(v6) => v6.segments()[0] & 0xffc0 == 0xfe80, + _ => false, + } +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 2777eaa..ba5ef4a 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod color; pub mod file; pub mod formatter; +pub mod ip; pub mod logger; pub mod tauri; diff --git a/src/rspc/bindings.ts b/src/rspc/bindings.ts index 738e3ff..b71e906 100644 --- a/src/rspc/bindings.ts +++ b/src/rspc/bindings.ts @@ -304,7 +304,7 @@ async setDecoration(isDecorated: boolean) : Promise { /** user-defined types **/ -export type BackendError = "cpuInfoNotAvailable" | "storageInfoNotAvailable" | "memoryInfoNotAvailable" | "graphicInfoNotAvailable" | "networkInfoNotAvailable" | "networkUsageNotAvailable" +export type BackendError = "cpuInfoNotAvailable" | "storageInfoNotAvailable" | "memoryInfoNotAvailable" | "graphicInfoNotAvailable" | "networkInfoNotAvailable" | "networkUsageNotAvailable" | "unexpectedError" /** * - `file_id` : 画像ファイルID * - `image_data` : 画像データのBase64文字列 @@ -324,7 +324,7 @@ export type HardwareType = "cpu" | "memory" | "gpu" export type LineGraphColorStringSettings = { cpu: string; memory: string; gpu: string } export type MemoryInfo = { size: string; clock: number; clockUnit: string; memoryCount: number; totalSlots: number; memoryType: string } export type NameValue = { name: string; value: number } -export type NetworkInfo = { ipv4: string[]; ipv6: string[]; mac: string } +export type NetworkInfo = { description: string | null; macAddress: string | null; ipV4: string[]; ipV6: string[]; linkLocalIpV6: string[]; ipSubnet: string[]; defaultIpv4Gateway: string[]; defaultIpv6Gateway: string[] } export type ProcessInfo = { pid: number; name: string; cpuUsage: number; memoryUsage: number } export type SizeUnit = "B" | "KB" | "MB" | "GB" export type StorageInfo = { name: string; size: number; sizeUnit: SizeUnit; free: number; freeUnit: SizeUnit; storageType: DiskKind; fileSystem: string } diff --git a/src/template/Dashboard.tsx b/src/template/Dashboard.tsx index 3fe29f6..86231a5 100644 --- a/src/template/Dashboard.tsx +++ b/src/template/Dashboard.tsx @@ -238,12 +238,17 @@ const NetworkInfo = () => { <> {networkInfo.map((network) => { return ( -
+
From 4e939907242702d7723d6d9e6423948023f81cc9 Mon Sep 17 00:00:00 2001 From: shm Date: Tue, 31 Dec 2024 16:20:03 +0900 Subject: [PATCH 3/6] Fix: Unit Test --- src-tauri/src/tests/utils/ip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/tests/utils/ip.rs b/src-tauri/src/tests/utils/ip.rs index f274b1b..3614553 100644 --- a/src-tauri/src/tests/utils/ip.rs +++ b/src-tauri/src/tests/utils/ip.rs @@ -11,7 +11,7 @@ mod tests { // グローバルIPv6アドレス let ip = IpAddr::V6("2400:4051::1".parse::().unwrap()); - assert_eq!(is_unicast_link_local(&ip), true); + assert_eq!(is_unicast_link_local(&ip), false); // IPv6マルチキャスト let ip = IpAddr::V6("ff02::1".parse::().unwrap()); From 0358fc7101fb6f551ff5ffd9f7be8f4a9788a40d Mon Sep 17 00:00:00 2001 From: shm Date: Tue, 31 Dec 2024 18:26:17 +0900 Subject: [PATCH 4/6] Add: Network Info Table --- package-lock.json | 62 +++++++++++ package.json | 1 + src/components/ui/accordion.tsx | 58 +++++++++++ src/i18n/en.json | 1 + src/i18n/ja.json | 1 + src/template/Dashboard.tsx | 177 ++++++++++++++++++++++++-------- 6 files changed, 259 insertions(+), 41 deletions(-) create mode 100644 src/components/ui/accordion.tsx diff --git a/package-lock.json b/package-lock.json index 054bf55..c87d591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.1", "@phosphor-icons/react": "^2.1.7", + "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-label": "^2.1.1", @@ -1117,6 +1118,37 @@ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz", + "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", @@ -1170,6 +1202,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", + "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", diff --git a/package.json b/package.json index aa12429..084606b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.1", "@phosphor-icons/react": "^2.1.7", + "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-label": "^2.1.1", diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..14142a8 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client"; + +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 1db2d38..af38f64 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -20,6 +20,7 @@ "process": "Process", "reset": "Reset", "storage": "Storage", + "network": "Network", "driveName": "Drive Name", "driveType": "Drive Type", "driveTotalSpace": "Drive Total Space", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 61b7deb..d3cde45 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -20,6 +20,7 @@ "process": "プロセス", "reset": "リセット", "storage": "ストレージ", + "network": "ネットワーク", "driveName": "ドライブ名", "driveType": "ドライブタイプ", "driveTotalSpace": "ドライブ容量", diff --git a/src/template/Dashboard.tsx b/src/template/Dashboard.tsx index d41ed0a..360cddc 100644 --- a/src/template/Dashboard.tsx +++ b/src/template/Dashboard.tsx @@ -12,6 +12,12 @@ import { } from "@/components/charts/Bar"; import { DoughnutChart } from "@/components/charts/DoughnutChart"; import { ProcessesTable } from "@/components/charts/ProcessTable"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { minOpacity } from "@/consts"; import type { StorageInfo } from "@/rspc/bindings"; import type { NameValues } from "@/types/hardwareDataType"; @@ -226,7 +232,8 @@ const StorageDataInfo = () => { }; const NetworkInfo = () => { - const { t } = useTranslation(); + //const { t } = useTranslation(); + const { settings } = useSettingsAtom(); const { networkInfo, initNetwork } = useHardwareInfoAtom(); // biome-ignore lint/correctness/useExhaustiveDependencies: @@ -238,26 +245,112 @@ const NetworkInfo = () => { <> {networkInfo.map((network) => { return ( -
- +
-
+ > + + + +
+

{network.description ?? "No description"}

+

+ {network.ipV4[0] ?? "No IP Address"} +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ MAC Address + + {network.macAddress ?? "No MAC Address"} +
IPv4 + {network.ipV4.map((ip) => ( +

{ip}

+ ))} +
+ IPv4 Subnet Mask + + {network.ipSubnet.map((subnet) => ( +

{subnet}

+ ))} +
+ IPv4 Gateway + + {network.defaultIpv4Gateway.map((gateway) => ( +

{gateway}

+ ))} +
IPv6 + {network.ipV6.map((ip) => ( +

{ip}

+ ))} +
+ Link Local IPv6 + + {network.linkLocalIpV6.map((ip) => ( +

{ip}

+ ))} +
+ IPv6 Gateway + + {network.defaultIpv6Gateway.map((gateway) => ( +

{gateway}

+ ))} +
+
+
+
+
+ ); })} ); }; +type DataTypeKey = "cpu" | "memory" | "storage" | "gpu" | "network" | "process"; + const Dashboard = () => { const { hardwareInfo } = useHardwareInfoAtom(); const { init } = useHardwareInfoAtom(); @@ -268,34 +361,36 @@ const Dashboard = () => { init(); }, []); - const hardwareInfoListLeft: { key: string; component: JSX.Element }[] = [ - hardwareInfo.cpu && { key: "cpuInfo", component: }, - hardwareInfo.memory && { key: "memoryInfo", component: }, - hardwareInfo.storage && hardwareInfo.storage.length > 0 - ? { - key: "storageInfo", - component: , - } - : undefined, - ].filter((x) => x != null); - - const hardwareInfoListRight: { key: string; component: JSX.Element }[] = [ - hardwareInfo.gpus && { key: "gpuInfo", component: }, - { - key: "processesTable", - component: , + const hardwareInfoListLeft: { key: DataTypeKey; component: JSX.Element }[] = [ + hardwareInfo.cpu && { key: "cpu", component: }, + hardwareInfo.memory && { key: "memory", component: }, + hardwareInfo.storage.length > 0 && { + key: "storage", + component: , }, - { - key: "networkInfo", - component: , - }, - ].filter((x) => x != null); + ].filter((x): x is { key: DataTypeKey; component: JSX.Element } => x != null); + + const hardwareInfoListRight: { key: DataTypeKey; component: JSX.Element }[] = + [ + hardwareInfo.gpus && { key: "gpu", component: }, + { + key: "process", + component: , + }, + { + key: "network", + component: , + }, + ].filter( + (x): x is { key: DataTypeKey; component: JSX.Element } => x != null, + ); - const dataAreaKey2Title: Record = { - cpuInfo: "CPU", - memoryInfo: "RAM", - storageInfo: t("shared.storage"), - gpuInfo: "GPU", + const dataAreaKey2Title: Partial> = { + cpu: "CPU", + memory: "RAM", + storage: t("shared.storage"), + gpu: "GPU", + network: t("shared.network"), }; return ( @@ -312,7 +407,7 @@ const Dashboard = () => { {component} From 266f617dd505acdd9ff27f8a938ec8ef8b28c83f Mon Sep 17 00:00:00 2001 From: shm Date: Wed, 1 Jan 2025 14:15:29 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E5=91=BD=E5=90=8D=E3=81=AE=E5=BE=AE?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/services/wmi_service.rs | 6 +++--- src-tauri/src/structs/hardware.rs | 6 +++--- src/rspc/bindings.ts | 2 +- src/template/Dashboard.tsx | 9 +++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/services/wmi_service.rs b/src-tauri/src/services/wmi_service.rs index e1f1697..d2df3c4 100644 --- a/src-tauri/src/services/wmi_service.rs +++ b/src-tauri/src/services/wmi_service.rs @@ -169,9 +169,9 @@ pub fn get_network_info() -> Result, String> { Ok(NetworkInfo { description: Some(adapter.description.unwrap_or_default()), mac_address: Some(adapter.mac_address.unwrap_or_default()), - ip_v4: ipv4.into_iter().map(|ip| ip.to_string()).collect(), - ip_v6: normal_ipv6.into_iter().map(|ip| ip.to_string()).collect(), - link_local_ip_v6: link_local_ipv6 + ipv4: ipv4.into_iter().map(|ip| ip.to_string()).collect(), + ipv6: normal_ipv6.into_iter().map(|ip| ip.to_string()).collect(), + link_local_ipv6: link_local_ipv6 .into_iter() .map(|ip| ip.to_string()) .collect(), diff --git a/src-tauri/src/structs/hardware.rs b/src-tauri/src/structs/hardware.rs index 7fd4d3e..94f6f97 100644 --- a/src-tauri/src/structs/hardware.rs +++ b/src-tauri/src/structs/hardware.rs @@ -41,9 +41,9 @@ pub struct StorageInfo { pub struct NetworkInfo { pub description: Option, pub mac_address: Option, - pub ip_v4: Vec, - pub ip_v6: Vec, - pub link_local_ip_v6: Vec, + pub ipv4: Vec, + pub ipv6: Vec, + pub link_local_ipv6: Vec, pub ip_subnet: Vec, pub default_ipv4_gateway: Vec, pub default_ipv6_gateway: Vec, diff --git a/src/rspc/bindings.ts b/src/rspc/bindings.ts index b71e906..2c229c0 100644 --- a/src/rspc/bindings.ts +++ b/src/rspc/bindings.ts @@ -324,7 +324,7 @@ export type HardwareType = "cpu" | "memory" | "gpu" export type LineGraphColorStringSettings = { cpu: string; memory: string; gpu: string } export type MemoryInfo = { size: string; clock: number; clockUnit: string; memoryCount: number; totalSlots: number; memoryType: string } export type NameValue = { name: string; value: number } -export type NetworkInfo = { description: string | null; macAddress: string | null; ipV4: string[]; ipV6: string[]; linkLocalIpV6: string[]; ipSubnet: string[]; defaultIpv4Gateway: string[]; defaultIpv6Gateway: string[] } +export type NetworkInfo = { description: string | null; macAddress: string | null; ipv4: string[]; ipv6: string[]; linkLocalIpv6: string[]; ipSubnet: string[]; defaultIpv4Gateway: string[]; defaultIpv6Gateway: string[] } export type ProcessInfo = { pid: number; name: string; cpuUsage: number; memoryUsage: number } export type SizeUnit = "B" | "KB" | "MB" | "GB" export type StorageInfo = { name: string; size: number; sizeUnit: SizeUnit; free: number; freeUnit: SizeUnit; storageType: DiskKind; fileSystem: string } diff --git a/src/template/Dashboard.tsx b/src/template/Dashboard.tsx index 360cddc..e823e41 100644 --- a/src/template/Dashboard.tsx +++ b/src/template/Dashboard.tsx @@ -264,8 +264,9 @@ const NetworkInfo = () => {

{network.description ?? "No description"}

+ {/** この部分にネットワーク使用量を表示 */}

- {network.ipV4[0] ?? "No IP Address"} + {network.ipv4[0] ?? "No IP Address"}

@@ -283,7 +284,7 @@ const NetworkInfo = () => { IPv4 - {network.ipV4.map((ip) => ( + {network.ipv4.map((ip) => (

{ip}

))} @@ -311,7 +312,7 @@ const NetworkInfo = () => { IPv6 - {network.ipV6.map((ip) => ( + {network.ipv6.map((ip) => (

{ip}

))} @@ -321,7 +322,7 @@ const NetworkInfo = () => { Link Local IPv6 - {network.linkLocalIpV6.map((ip) => ( + {network.linkLocalIpv6.map((ip) => (

{ip}

))} From aa4441e811664707fcef032b5a80c4314bb92ee8 Mon Sep 17 00:00:00 2001 From: shm Date: Wed, 1 Jan 2025 14:26:44 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Add:=20=E3=83=8D=E3=83=83=E3=83=88=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E6=83=85=E5=A0=B1=E3=81=AEi18n=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/en.json | 7 +++++++ src/i18n/ja.json | 7 +++++++ src/template/Dashboard.tsx | 21 +++++++++++++-------- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index af38f64..92e35dd 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -27,6 +27,13 @@ "driveUsedSpace": "Drive Used Space", "driveFreeSpace": "Drive Free Space", "driveFileSystem": "Drive File System", + "macAddress": "MAC Address", + "subnetMask": "Subnet Mask", + "gateway": "Gateway", + "linkLocal": "Link Local", + "ipv4": "IPv4", + "ipv6": "IPv6", + "address": "Address", "used": "Used", "free": "Free", "other": "Other" diff --git a/src/i18n/ja.json b/src/i18n/ja.json index d3cde45..85b7a42 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -27,6 +27,13 @@ "driveUsedSpace": "ドライブ使用量", "driveFreeSpace": "ドライブ空き容量", "driveFileSystem": "ファイルシステム", + "macAddress": "MAC アドレス", + "subnetMask": "サブネットマスク", + "gateway": "ゲートウェイ", + "linkLocal": "リンクローカル", + "ipv4": "IPv4", + "ipv6": "IPv6", + "address": "アドレス", "used": "使用中", "free": "空き", "other": "その他" diff --git a/src/template/Dashboard.tsx b/src/template/Dashboard.tsx index e823e41..0ca4551 100644 --- a/src/template/Dashboard.tsx +++ b/src/template/Dashboard.tsx @@ -232,7 +232,7 @@ const StorageDataInfo = () => { }; const NetworkInfo = () => { - //const { t } = useTranslation(); + const { t } = useTranslation(); const { settings } = useSettingsAtom(); const { networkInfo, initNetwork } = useHardwareInfoAtom(); @@ -275,14 +275,16 @@ const NetworkInfo = () => { - MAC Address + {t("shared.macAddress")} {network.macAddress ?? "No MAC Address"} - IPv4 + + {t("shared.ipv4")} + {network.ipv4.map((ip) => (

{ip}

@@ -291,7 +293,7 @@ const NetworkInfo = () => { - IPv4 Subnet Mask + {t("shared.ipv4")} {t("shared.subnetMask")} {network.ipSubnet.map((subnet) => ( @@ -301,7 +303,7 @@ const NetworkInfo = () => { - IPv4 Gateway + {t("shared.ipv4")} {t("shared.gateway")} {network.defaultIpv4Gateway.map((gateway) => ( @@ -310,7 +312,9 @@ const NetworkInfo = () => { - IPv6 + + {t("shared.ipv6")} + {network.ipv6.map((ip) => (

{ip}

@@ -319,7 +323,8 @@ const NetworkInfo = () => { - Link Local IPv6 + {t("shared.linkLocal")} {t("shared.ipv6")}{" "} + {t("shared.address")} {network.linkLocalIpv6.map((ip) => ( @@ -329,7 +334,7 @@ const NetworkInfo = () => { - IPv6 Gateway + {t("shared.ipv6")} {t("shared.gateway")} {network.defaultIpv6Gateway.map((gateway) => (