diff --git a/AppConfig.json b/AppConfig.json index 9e5e065..980040a 100644 --- a/AppConfig.json +++ b/AppConfig.json @@ -20,11 +20,6 @@ "https://api.inn-studio.com/download/?id=xprober-browser-benchmarks" ], "APP_CONFIG_URL_DEV": "http://localhost:8000/AppConfig.json", - "APP_TEMPERATURE_SENSOR_URL": "http://127.0.0.1", - "APP_TEMPERATURE_SENSOR_PORTS": [ - 2048, - 4096 - ], "AUTHOR_NAME": "INN STUDIO", "LATEST_PHP_STABLE_VERSION": "8", "LATEST_NGINX_STABLE_VERSION": "1.22.0", diff --git a/biome.jsonc b/biome.jsonc index 2742b89..fe13fde 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,16 +1,44 @@ { + "extends": [ + "ultracite/biome/core", + "ultracite/biome/react", + "ultracite/biome/remix", + "ultracite/biome/vitest" + ], "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, "linter": { "rules": { + "a11y": { + "useKeyWithClickEvents": "warn" + }, "complexity": { - "noExcessiveCognitiveComplexity": "off" + "noExcessiveCognitiveComplexity": "warn" + }, + "suspicious": { + "noAlert": "warn", + "noConsole": "warn" + }, + "performance": { + "noAwaitInLoops": "off" }, "security": { "noDangerouslySetInnerHtml": "off" + }, + "style": { + "useNamingConvention": "warn", + "noMagicNumbers": "warn", + "useConsistentTypeDefinitions": { + "level": "warn", + "options": { + "style": "type" + } + } } } - }, - "extends": [ - "ultracite" - ] + } } \ No newline at end of file diff --git a/compiler/Compiler.php b/compiler/Compiler.php index c886ecf..2d8902e 100644 --- a/compiler/Compiler.php +++ b/compiler/Compiler.php @@ -1,110 +1,162 @@ ROOT = $dir; - $this->BASE_DIR = "{$dir}/src"; - $this->COMPONENTS_DIR = "{$this->BASE_DIR}/Components"; - $this->COMPILE_FILE_PATH = $this->isDev() ? "{$dir}/dev/api.php" : "{$dir}/dist/prober.php"; + $this->baseDir = "{$this->root}/src"; + $this->componentsDir = "{$this->baseDir}/Components"; - // generate config - new ConfigGeneration([ - 'phpConfigPath' => "{$this->COMPONENTS_DIR}/Config/ConfigApi.php", - 'configPath' => "{$this->ROOT}/AppConfig.json", - 'configPathDev' => "{$this->ROOT}/dev/AppConfig.json", - ]); + // 根据开发模式还是生产模式决定输出路径 + // Determine compile target path based on environment mode + $this->compileFilePath = $this->isDev() + ? "{$this->root}/dev/api.php" + : "{$this->root}/dist/prober.php"; + } + /** + * 执行编译打包逻辑 + * Run the compile and pack process. + */ + public function compile(): void + { echo "Compile starting...\n"; + // 1. 初始化生成配置 / 1. Initialize and generate configuration + try { + $configGen = new ConfigGeneration( + phpConfigPath: "{$this->componentsDir}/Config/ConfigApi.php", + configPath: "{$this->root}/AppConfig.json", + configPathDev: "{$this->root}/dev/AppConfig.json" + ); + $configGen->generate(); + } catch (Exception $e) { + echo '[Compiler Error] Config gen failed: ' . $e->getMessage() . "\n"; + + return; + } + $code = ''; + // 2. 生产模式下,合并所有的 PHP 组件文件 + // 2. In production mode, merge all PHP component files if ( ! $this->isDev()) { - foreach ($this->yieldFiles($this->COMPONENTS_DIR) as $filePath) { - if (is_dir($filePath) || ! str_contains($filePath, '.php')) { - continue; - } - - $content = $this->getCodeViaFilePath($filePath); - $code .= $content; + foreach ($this->yieldPhpFiles($this->componentsDir) as $filePath) { + $code .= $this->getCodeViaFilePath($filePath); } } + // 3. 组装前置预定义代码、主体代码与加载器 + // 3. Assemble pre-definitions, body code, and loader $preDefineCode = $this->preDefine([ $this->genTimerCode(), $this->genDevMode(), $this->genDirPath(), $this->genVendorCode(), ]); + $code = "loader(); - $code = preg_replace("/(\r|\n)+/", "\n", $code); + // 规范化换行符 / Normalize line breaks + $code = preg_replace("/(\r\n|\r|\n)+/", "\n", $code); + + // 4. 写入初步打包的代码 / 4. Write compiled code into target file if ( ! $this->writeFile($code)) { - throw new Exception('Failed to write file.'); - } - if ( ! $this->isDev()) { - new ScriptGeneration([ - 'scriptFilePath' => "{$this->ROOT}/.tmp/app.js", - 'distFilePath' => $this->COMPILE_FILE_PATH, - ]); - new StyleGeneration([ - 'styleFilePath' => "{$this->ROOT}/.tmp/app.css", - 'distFilePath' => $this->COMPILE_FILE_PATH, - ]); + throw new Exception('[Compiler Error] Failed to write compiled source code.'); } + // 5. 生产模式下,注入前端 JS/CSS 到单文件中 + // 5. In production mode, inject frontend JS/CSS assets into the single file if ( ! $this->isDev()) { - // if ($this->isDebug()) { - $this->writeFile(file_get_contents($this->COMPILE_FILE_PATH)); - // } else { - // $this->writeFile(php_strip_whitespace($this->COMPILE_FILE_PATH)); - // } + try { + $scriptGen = new ScriptGeneration( + scriptFilePath: "{$this->root}/.tmp/app.js", + distFilePath: $this->compileFilePath + ); + $scriptGen->generate(); + + $styleGen = new StyleGeneration( + styleFilePath: "{$this->root}/.tmp/app.css", + distFilePath: $this->compileFilePath + ); + $styleGen->generate(); + } catch (Exception $e) { + echo '[Compiler Error] Asset injection failed: ' . $e->getMessage() . "\n"; + + return; + } + + // 可选:如果不是 Debug 状态,可以对单文件做进一步清理(如 php_strip_whitespace) + // Optional: If not in debug mode, further clean up the file (e.g., php_strip_whitespace) + $finalContent = file_get_contents($this->compileFilePath); + if (false !== $finalContent) { + $this->writeFile($finalContent); + } } - echo 'Compiled!'; + echo "Compiled successfully!\n"; } + /** + * 读取并清理子文件的 PHP 标签 + * Read and clean PHP tags from target file. + */ private function getCodeViaFilePath(string $filePath): string { - $code = ''; - - echo "Packing `{$filePath}..."; + echo "Packing `{$filePath}`... "; $code = file_get_contents($filePath); - $code = trim($code, "\n"); + if (false === $code) { + echo "FAILED\n"; + + return ''; + } + + $code = trim($code); + + // 💡 优化:使用正则安全去除开头的 isDev() ? 'true' : 'false'; - - return <<isDev() ? 'true' : 'false') . ');'; } private function genDirPath(): string { - return <<<'PHP' -\define('XPROBER_DIR', __DIR__); -PHP; + return "\\define('XPROBER_DIR', __DIR__);"; } private function genTimerCode(): string { - return <<<'PHP' -\define('XPROBER_TIMER', \microtime(true)); -PHP; + return "\\define('XPROBER_TIMER', \\microtime(true));"; } private function loader(): string { - $dirs = glob($this->COMPONENTS_DIR . '/*'); - - if ( ! $dirs) { - return ''; - } $bootstrapDir = $this->isDev() ? 'dirname(__DIR__)' : '__DIR__'; - $files = []; - $files[] = << + */ + private function yieldPhpFiles(string $dir): Generator { - if (is_dir($dir)) { - $dh = opendir($dir); - - if ( ! $dh) { - yield false; - } - - while (false !== ($file = readdir($dh))) { - if ('.' === $file || '..' === $file) { - continue; - } - - $filePath = "{$dir}/{$file}"; - - if (is_dir($filePath)) { - foreach ($this->yieldFiles($filePath) as $yieldFilepath) { - yield $yieldFilepath; - } - } else { - yield $filePath; - } - } - - closedir($dh); + if ( ! is_dir($dir)) { + return; } - yield $dir; + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); + + /** @var SplFileInfo $file */ + foreach ($iterator as $file) { + if ($file->isFile() && 'php' === $file->getExtension()) { + yield $file->getPathname(); + } + } } private function writeFile(string $data): bool { - $dir = \dirname($this->COMPILE_FILE_PATH); + $dir = \dirname($this->compileFilePath); - if ( ! is_dir($dir)) { - mkdir($dir, 0755, true); + if ( ! is_dir($dir) && ! mkdir($dir, 0755, true) && ! is_dir($dir)) { + return false; } - return (bool) file_put_contents($this->COMPILE_FILE_PATH, $data); + return false !== file_put_contents($this->compileFilePath, $data); } } diff --git a/compiler/ConfigGeneration.php b/compiler/ConfigGeneration.php index 59b7580..4de8d1c 100644 --- a/compiler/ConfigGeneration.php +++ b/compiler/ConfigGeneration.php @@ -1,59 +1,89 @@ $this->phpConfigPath, - 'configPath' => $this->configPath, - 'configPathDev' => $this->configPathDev, - ] = $args; - + // 验证主配置文件是否存在 / Validate if the main config file exists if ( ! is_file($this->configPath)) { - $this->die("File invalid: {$this->configPath}"); + throw new InvalidArgumentException("[ConfigGeneration] File invalid: {$this->configPath}"); } - if ( ! $this->genPhpConfig()) { - $this->die('Error: can not generate content to dist.'); - } + // 生成 PHP 配置文件 / Generate the PHP configuration file + $this->genPhpConfig(); + // 备份/同步配置文件到开发环境 / Copy/sync config file to dev environment $this->copyConfigToTmp(); - $this->die('PHP config file generated successful.', false); + echo "[ConfigGeneration] PHP config file generated successfully.\n"; } - private function copyConfigToTmp(): bool + /** + * 复制配置文件到开发/临时路径 + * Copy the configuration file to the dev/temporary path. + */ + private function copyConfigToTmp(): void { - return copy($this->configPath, $this->configPathDev); + if ( ! copy($this->configPath, $this->configPathDev)) { + throw new RuntimeException("[ConfigGeneration] Failed to copy config to dev path: {$this->configPathDev}"); + } } - private function genPhpConfig(): bool + /** + * 解析 JSON 并生成对应的 PHP 配置类 + * Parse JSON and generate the corresponding PHP configuration class. + */ + private function genPhpConfig(): void { - $config = file_get_contents($this->configPath) ?: ''; + $configRaw = file_get_contents($this->configPath); - if ( ! $config) { - return false; + if (false === $configRaw || '' === $configRaw) { + throw new RuntimeException("[ConfigGeneration] Failed to read or empty config file: {$this->configPath}"); } - $config = json_decode($config, true); + // 解析 JSON 数据 / Parse JSON data + $configData = json_decode($configRaw, true); - if ( ! $config) { - return false; + if (null === $configData && \JSON_ERROR_NONE !== json_last_error()) { + throw new RuntimeException('[ConfigGeneration] JSON decode error: ' . json_last_error_msg()); } - $config = var_export($config, true); + // 将数组转换为一等 PHP 代码格式 / Export array into clean PHP code format + $exportedConfig = var_export($configData, true); + $configContent = <<phpConfigPath, $configContent); - } - - private function die(string $msg, bool $die = true): void - { - $msg = "[StyleGeneration] {$msg}\n"; - - if ($die) { - exit($msg); + // 写入 PHP 文件 / Write to the PHP file + if (false === file_put_contents($this->phpConfigPath, $configContent)) { + throw new RuntimeException("[ConfigGeneration] Error: cannot write generated content to {$this->phpConfigPath}"); } - - echo $msg; } } diff --git a/compiler/ScriptGeneration.php b/compiler/ScriptGeneration.php index a14e636..9e2a7bc 100644 --- a/compiler/ScriptGeneration.php +++ b/compiler/ScriptGeneration.php @@ -1,61 +1,97 @@ $this->scriptFilePath, - 'distFilePath' => $this->distFilePath, - ] = $args; - - if ( ! is_file($this->scriptFilePath)) { - $this->die("File not found: {$this->scriptFilePath}"); - } - - if ( ! is_file($this->distFilePath)) { - $this->die("File not found: {$this->distFilePath}"); - } - - if ( ! $this->setScript($this->getScript())) { - $this->die('Error: can not write script content to dist.'); - } - - $this->die('Script content wrote successful.', false); + /** + * 使用 PHP 8.0+ 构造函数属性提升,直接声明和初始化属性。 + * Use PHP 8.0+ Constructor Property Promotion to declare and initialize properties. + */ + public function __construct( + private string $scriptFilePath, + private string $distFilePath + ) { } + /** + * 执行脚本生成逻辑 + * Execute the script generation logic. + * + * @throws InvalidArgumentException 当文件不存在时抛出 | Thrown when files do not exist + * @throws RuntimeException 当读取或写入失败时抛出 | Thrown when read/write operations fail + */ + public function generate(): void + { + // 验证源脚本文件是否存在 / Validate if the source script file exists + if ( ! is_file($this->scriptFilePath)) { + throw new InvalidArgumentException("[ScriptGeneration] Source file not found: {$this->scriptFilePath}"); + } + + // 验证目标模板文件是否存在 / Validate if the destination file exists + if ( ! is_file($this->distFilePath)) { + throw new InvalidArgumentException("[ScriptGeneration] Destination file not found: {$this->distFilePath}"); + } + + $scriptContent = $this->getScript(); + + $this->setScript($scriptContent); + + echo "[ScriptGeneration] Script content written successfully.\n"; + } + + /** + * 获取源脚本内容 + * Get the content of the source script. + */ private function getScript(): string { - return (string) file_get_contents($this->scriptFilePath); - } + $content = file_get_contents($this->scriptFilePath); - private function setScript(string $script): bool - { - $dist = (string) file_get_contents($this->distFilePath); - - if ( ! $dist) { - return false; + if (false === $content) { + throw new RuntimeException("[ScriptGeneration] Failed to read source file: {$this->scriptFilePath}"); } - $dist = str_replace('{{X_SCRIPT}}', $script, $dist); - - return (bool) file_put_contents($this->distFilePath, $dist); + return $content; } - private function die(string $msg, bool $die = true): void + /** + * 将脚本内容替换并写入到目标文件中 + * Replace placeholders and write content to the destination file. + */ + private function setScript(string $script): void { - $msg = "[ScriptGeneration] {$msg}\n"; + $distContent = file_get_contents($this->distFilePath); - if ($die) { - exit($msg); + // 如果读取失败,或者文件为空,抛出异常 + // If reading fails or the file is empty, throw an exception + if (false === $distContent || '' === $distContent) { + throw new RuntimeException("[ScriptGeneration] Destination file is empty or unreadable: {$this->distFilePath}"); } - echo $msg; + // 检查是否存在占位符(可选优化:防止无效替换) + // Check if the placeholder exists (Optional optimization: prevent redundant writes) + if ( ! str_contains($distContent, '{{X_SCRIPT}}')) { + throw new RuntimeException('[ScriptGeneration] Placeholder {{X_SCRIPT}} not found in destination file.'); + } + + $updatedContent = str_replace('{{X_SCRIPT}}', $script, $distContent); + + // 执行写入操作 / Perform the write operation + $result = file_put_contents($this->distFilePath, $updatedContent); + + if (false === $result) { + throw new RuntimeException('[ScriptGeneration] Failed to write script content to destination.'); + } } } diff --git a/compiler/StyleGeneration.php b/compiler/StyleGeneration.php index 4483a64..c6795ba 100644 --- a/compiler/StyleGeneration.php +++ b/compiler/StyleGeneration.php @@ -2,60 +2,92 @@ namespace InnStudio\Prober\Compiler; +use InvalidArgumentException; +use RuntimeException; + final class StyleGeneration { - private $styleFilePath = ''; - - private $distFilePath = ''; - - public function __construct(array $args) - { - [ - 'styleFilePath' => $this->styleFilePath, - 'distFilePath' => $this->distFilePath, - ] = $args; - - if ( ! is_file($this->styleFilePath)) { - $this->die("File not found: {$this->styleFilePath}"); + /** + * 构造函数:仅用于接收和校验基础参数 + * Constructor: Only used to receive and validate basic parameters. + */ + public function __construct( + private string $styleFilePath, + private string $distFilePath + ) { + if (empty($this->styleFilePath) || empty($this->distFilePath)) { + throw new InvalidArgumentException('[StyleGeneration] Missing required file paths in arguments.'); } - - if ( ! is_file($this->distFilePath)) { - $this->die("File not found: {$this->distFilePath}"); - } - - if ( ! $this->setStyle($this->getStyle())) { - $this->die('Error: can not write script content to dist.'); - } - - $this->die('Script content wrote successful.', false); } + /** + * 执行核心编译逻辑 + * Execute the core compilation logic. + * + * @throws RuntimeException 如果文件不存在或写入失败 / If files do not exist or writing fails + */ + public function generate(): void + { + // 校验源样式文件是否存在 + // Validate if the source style file exists. + if ( ! is_file($this->styleFilePath)) { + throw new RuntimeException("[StyleGeneration] File not found: {$this->styleFilePath}"); + } + + // 校验目标文件是否存在 + // Validate if the target dist file exists. + if ( ! is_file($this->distFilePath)) { + throw new RuntimeException("[StyleGeneration] File not found: {$this->distFilePath}"); + } + + $styleContent = $this->getStyle(); + + // 执行替换并写入文件 + // Perform replacement and write to file. + if ($this->setStyle($styleContent)) { + echo "[StyleGeneration] Script content wrote successfully.\n"; + } else { + throw new RuntimeException('[StyleGeneration] Error: Cannot write script content to dist.'); + } + } + + /** + * 获取样式文件内容 + * Get the content of the style file. + */ private function getStyle(): string { - return (string) file_get_contents($this->styleFilePath); + $content = file_get_contents($this->styleFilePath); + + // 严格检查文件是否成功读取 + // Strictly check if the file was read successfully. + if (false === $content) { + throw new RuntimeException("[StyleGeneration] Failed to read style file: {$this->styleFilePath}"); + } + + return $content; } + /** + * 将样式内容替换到目标文件 + * Replace the style content into the target file. + */ private function setStyle(string $style): bool { - $dist = (string) file_get_contents($this->distFilePath); + $dist = file_get_contents($this->distFilePath); - if ( ! $dist) { + // 严格检查目标文件内容是否成功读取,且不为空 + // Strictly check if the target file content was read successfully and is not empty. + if (false === $dist || '' === $dist) { return false; } + // 替换占位符 + // Replace the placeholder. $dist = str_replace('{{X_STYLE}}', $style, $dist); - return (bool) file_put_contents($this->distFilePath, $dist); - } - - private function die(string $msg, bool $die = true): void - { - $msg = "[StyleGeneration] {$msg}\n"; - - if ($die) { - exit($msg); - } - - echo $msg; + // file_put_contents 失败时返回 false,成功时返回写入的字节数 + // file_put_contents returns false on failure, or the number of bytes written on success. + return false !== file_put_contents($this->distFilePath, $dist); } } diff --git a/dev.vite.config.mjs b/dev.vite.config.mjs index ebaa0eb..66c9b89 100644 --- a/dev.vite.config.mjs +++ b/dev.vite.config.mjs @@ -1,39 +1,16 @@ -import { dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import react from '@vitejs/plugin-react'; -import { defineConfig, loadEnv } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import react from "@vitejs/plugin-react"; +import { defineConfig, loadEnv } from "vite"; const REGEX = /^\/api/; export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ''); + const env = loadEnv(mode, process.cwd(), ""); return { - root: './dev', - envDir: './', - server: { - proxy: { - '/api': { - target: 'http://localhost:8000/api.php', - changeOrigin: true, - rewrite: (path) => path.replace(REGEX, ''), - }, - }, - }, - resolve: { - alias: { - '@': `${dirname(fileURLToPath(import.meta.url))}/src`, - }, - }, - css: { - modules: { - generateScopedName: '[name]__[local]_[hash]', - }, - }, - plugins: [react(), tsconfigPaths()], build: { - outDir: '../dist', manifest: true, - target: 'esnext', + outDir: "../dist", + target: "esnext", // rollupOptions: { // external: ['react', 'react-dom'], // output: { @@ -48,8 +25,31 @@ export default defineConfig(({ mode }) => { // input: new URL('./src/main.tsx', import.meta.url).pathname, // }, }, + css: { + modules: { + generateScopedName: "[name]__[local]_[hash]", + }, + }, define: { VITE_PORT: JSON.stringify(env.VITE_PORT), }, + envDir: "./", + plugins: [react()], + resolve: { + alias: { + "@": `${dirname(fileURLToPath(import.meta.url))}/src`, + }, + tsconfigPaths: true, + }, + root: "./dev", + server: { + proxy: { + "/api": { + changeOrigin: true, + rewrite: (path) => path.replace(REGEX, ""), + target: "http://localhost:8000/api.php", + }, + }, + }, }; }); diff --git a/dev/AppConfig.json b/dev/AppConfig.json index 9e5e065..980040a 100644 --- a/dev/AppConfig.json +++ b/dev/AppConfig.json @@ -20,11 +20,6 @@ "https://api.inn-studio.com/download/?id=xprober-browser-benchmarks" ], "APP_CONFIG_URL_DEV": "http://localhost:8000/AppConfig.json", - "APP_TEMPERATURE_SENSOR_URL": "http://127.0.0.1", - "APP_TEMPERATURE_SENSOR_PORTS": [ - 2048, - 4096 - ], "AUTHOR_NAME": "INN STUDIO", "LATEST_PHP_STABLE_VERSION": "8", "LATEST_NGINX_STABLE_VERSION": "1.22.0", diff --git a/dev/index.html b/dev/index.html index 52280a2..3f1b90b 100644 --- a/dev/index.html +++ b/dev/index.html @@ -3,14 +3,39 @@ - - - + + + Dev mode - - + + - -
Loading...
+
Loading...
\ No newline at end of file diff --git a/package.json b/package.json index b62fdfc..31ffa90 100644 --- a/package.json +++ b/package.json @@ -17,47 +17,30 @@ "defaults" ], "dependencies": { - "copy-to-clipboard": "^3.3.3", - "express": "^5.1.0", - "lucide-react": "^0.542.0", - "mobx": "^6.13.7", - "mobx-react-lite": "^4.1.0", - "polished": "^4.3.1", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-is": "^19.1.1", - "react-use": "^17.6.0" + "copy-to-clipboard": "^4.0.2", + "immer": "^11.1.11", + "lucide-react": "^1.23.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "zustand": "^5.0.14" }, "devDependencies": { - "@biomejs/biome": "2.2.2", - "@eslint/js": "^9.34.0", - "@types/node": "^24.3.0", - "@types/react": "^19.1.12", - "@types/react-dom": "^19.1.9", - "@typescript-eslint/eslint-plugin": "^8.41.0", - "@typescript-eslint/parser": "^8.41.0", - "@vitejs/plugin-react": "^5.0.2", - "css-loader": "^7.1.2", + "@biomejs/biome": "2.5.1", + "@types/node": "^26.1.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "css-loader": "^7.1.4", "deep-sort-object": "^1.0.2", - "eslint": "^9.34.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "fast-glob": "^3.3.3", - "gettext-parser": "^8.0.0", - "globals": "^16.3.0", - "mini-css-extract-plugin": "^2.9.4", - "prettier": "^3.6.2", - "sass": "^1.91.0", - "sass-loader": "^16.0.5", + "gettext-parser": "^9.0.2", + "mini-css-extract-plugin": "^2.10.2", + "sass": "^1.101.0", + "sass-loader": "^17.0.0", "style-loader": "^4.0.0", - "typescript": "^5.9.2", - "typescript-eslint": "^8.41.0", - "typescript-plugin-css-modules": "^5.2.0", - "ultracite": "^5.2.17", - "vite": "^7.1.3", - "vite-plugin-dts": "^4.5.4", - "vite-tsconfig-paths": "^5.1.4" + "typescript": "^6.0.3", + "ultracite": "^7.9.2", + "update-browserslist-db": "^1.2.3", + "vite": "^8.1.3", + "vite-plugin-dts": "^5.0.3" } -} +} \ No newline at end of file diff --git a/src/Components/Bootstrap/Render.php b/src/Components/Bootstrap/Render.php index e0576c9..7637883 100644 --- a/src/Components/Bootstrap/Render.php +++ b/src/Components/Bootstrap/Render.php @@ -35,14 +35,13 @@ final class Render --x-init-body-bg: hsl(0 0% 90%); --x-init-loading-bg: hsl(0 0% 90%); --x-init-loading-fg: hsl(0 0% 10%); - - @media (prefers-color-scheme: dark) { - --x-init-fg: hsl(0 0% 90%); - --x-init-body-fg: hsl(0 0% 90%); - --x-init-body-bg: hsl(0 0% 0%); - --x-init-loading-bg: hsl(0 0% 0%); - --x-init-loading-fg: hsl(0 0% 90%); - } +} +[data-theme="dark"] { + --x-init-fg: hsl(0 0% 90%); + --x-init-body-fg: hsl(0 0% 90%); + --x-init-body-bg: hsl(0 0% 0%); + --x-init-loading-bg: hsl(0 0% 0%); + --x-init-loading-fg: hsl(0 0% 90%); } @keyframes spin { to { diff --git a/src/Components/Bootstrap/components/global.scss b/src/Components/Bootstrap/components/global.scss index 342517d..180bdcb 100644 --- a/src/Components/Bootstrap/components/global.scss +++ b/src/Components/Bootstrap/components/global.scss @@ -1,10 +1,8 @@ :root { - // --x-html-bg: var(--x-fg); --x-fg: hsl(0 0% 10%); --x-body-fg: hsl(0 0% 10%); --x-body-bg: hsl(0 0% 90%); - @media (prefers-color-scheme: dark) { - // --x-html-bg: hsl(0, 0%, 0%); + &[data-theme="dark"] { --x-fg: hsl(0 0% 90%); --x-body-fg: hsl(0 0% 90%); --x-body-bg: hsl(0 0% 0%); @@ -24,7 +22,6 @@ html { body { display: grid; place-content: safe center; - vertical-align: middle; gap: var(--x-gutter); margin: 0; background: var(--x-body-bg); diff --git a/src/Components/Bootstrap/components/index.tsx b/src/Components/Bootstrap/components/index.tsx index 3f0c5b6..a181741 100644 --- a/src/Components/Bootstrap/components/index.tsx +++ b/src/Components/Bootstrap/components/index.tsx @@ -1,79 +1,96 @@ -import '@/Components/ColorScheme/components/config.scss'; -import { type FC, useEffect, useState } from 'react'; -import { ConfigStore } from '@/Components/Config/store.ts'; -import { DatabaseStore } from '@/Components/Database/components/store.ts'; -import { DiskUsageStore } from '@/Components/DiskUsage/components/store.ts'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { Footer } from '@/Components/Footer/components/index.tsx'; -import { Header } from '@/Components/Header/components/index.tsx'; -import { MyInfoStore } from '@/Components/MyInfo/components/store.ts'; -import { NetworkStatsStore } from '@/Components/NetworkStats/components/store.ts'; -import { NodesStore } from '@/Components/Nodes/components/store.ts'; -import { PhpExtensionsStore } from '@/Components/PhpExtensions/components/store.ts'; -import { PhpInfoStore } from '@/Components/PhpInfo/components/store.ts'; -import type { PollDataProps } from '@/Components/Poll/components/typings.ts'; -import { ServerInfoStore } from '@/Components/ServerInfo/components/store.ts'; -import { ServerStatusStore } from '@/Components/ServerStatus/components/store.ts'; -import { Toast } from '@/Components/Toast/components/index.tsx'; -import { UserConfigStore } from '@/Components/UserConfig/store.ts'; -import './global.scss'; -import { gettext } from '@/Components/Language/index.ts'; -import { Modules } from '@/Components/Module/components/index.tsx'; -import { Nav } from '@/Components/Nav/components/index.tsx'; -import { PollStore } from '@/Components/Poll/components/store.ts'; -import { TemperatureSensorStore } from '@/Components/TemperatureSensor/components/store.ts'; -import { ToastStore } from '@/Components/Toast/components/store.ts'; -import { UpdaterStore } from '@/Components/Updater/components/store.ts'; -import { BootstrapLoading } from './loading.tsx'; +import "@/Components/ColorScheme/components/config.scss"; +import { type FC, useEffect, useState } from "react"; +import { useConfigStore } from "@/Components/Config/store.ts"; +import { useDatabaseStore } from "@/Components/Database/components/store.ts"; +import { useDiskUsageStore } from "@/Components/DiskUsage/components/store.ts"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { Footer } from "@/Components/Footer/components/index.tsx"; +import { Header } from "@/Components/Header/components/index.tsx"; +import { useMyInfoStore } from "@/Components/MyInfo/components/store.ts"; +import { useNetworkStatsStore } from "@/Components/NetworkStats/components/store.ts"; +import { useNodesStore } from "@/Components/Nodes/components/store.ts"; +import { usePhpExtensionsStore } from "@/Components/PhpExtensions/components/store.ts"; +import { usePhpInfoStore } from "@/Components/PhpInfo/components/store.ts"; +import type { PollData } from "@/Components/Poll/components/types.ts"; +import { useServerInfoStore } from "@/Components/ServerInfo/components/store.ts"; +import { useServerStatusStore } from "@/Components/ServerStatus/components/store.ts"; +import { Toast } from "@/Components/Toast/components/index.tsx"; +import { useUserConfigStore } from "@/Components/UserConfig/store.ts"; +import "./global.scss"; +import { gettext } from "@/Components/Language/index.ts"; +import { Modules } from "@/Components/Module/components/index.tsx"; +import { Nav } from "@/Components/Nav/components/index.tsx"; +import { usePollStore } from "@/Components/Poll/components/store.ts"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { useTemperatureSensorStore } from "@/Components/TemperatureSensor/components/store.ts"; +import { useToastStore } from "@/Components/Toast/components/store.ts"; +import { useUpdaterStore } from "@/Components/Updater/components/store.ts"; +import { useInterval } from "@/Components/Utils/components/use-interval.ts"; +import { BootstrapLoading } from "./loading.tsx"; + +const TIMER = 2000; + export const Bootstrap: FC = () => { - const [loading, setLoading] = useState(true); - const { isUpdating } = UpdaterStore; - useEffect(() => { - let timeoutId: NodeJS.Timeout; - let isMounted = true; - const fetchData = async () => { - try { - if (isUpdating) { - return; - } - const { data, status } = await serverFetch('poll'); - if (data && status === 200) { - PollStore.setPollData(data); - ConfigStore.setPollData(data?.config); - UserConfigStore.setPollData(data?.userConfig); - DatabaseStore.setPollData(data?.database); - MyInfoStore.setPollData(data?.myInfo); - PhpInfoStore.setPollData(data?.phpInfo); - DiskUsageStore.setPollData(data?.diskUsage); - PhpExtensionsStore.setPollData(data?.phpExtensions); - NetworkStatsStore.setPollData(data?.networkStats); - ServerStatusStore.setPollData(data?.serverStatus); - ServerInfoStore.setPollData(data?.serverInfo); - NodesStore.setPollData(data?.nodes); - TemperatureSensorStore.setPollData(data?.temperatureSensor); - } else { - ToastStore.open( - gettext('Failed to fetch data. Please try again later.') - ); - } - if (loading) { - setLoading(false); - } - } finally { - if (isMounted) { - timeoutId = setTimeout(fetchData, 2000); - } + const [isLoading, setIsLoading] = useState(true); + const openToast = useToastStore((s) => s.open); + const isUpdating = useUpdaterStore((s) => s.isUpdating); + + // 轮询间隔控制 + const pollDelay = isUpdating ? null : TIMER; + + // 核心数据分发逻辑 + const fetchPollData = async () => { + if (isUpdating) { + return; + } + + try { + const { data, status } = await serverFetch("poll"); + + if (status === OK && data) { + usePollStore.getState().setPollData(data); + useConfigStore.getState().setPollData(data?.config); + useUserConfigStore.getState().setPollData(data?.userConfig); + useDatabaseStore.getState().setPollData(data?.database); + useMyInfoStore.getState().setPollData(data?.myInfo); + usePhpInfoStore.getState().setPollData(data?.phpInfo); + useDiskUsageStore.getState().setPollData(data?.diskUsage); + usePhpExtensionsStore.getState().setPollData(data?.phpExtensions); + useNetworkStatsStore.getState().setPollData(data?.networkStats); + useServerStatusStore.getState().setPollData(data?.serverStatus); + useServerInfoStore.getState().setPollData(data?.serverInfo); + useNodesStore.getState().setPollData(data?.nodes); + useTemperatureSensorStore + .getState() + .setPollData(data?.temperatureSensor); + } else { + openToast(gettext("Failed to fetch data. Please try again later.")); } - }; - fetchData(); - return () => { - isMounted = false; - clearTimeout(timeoutId); - }; - }, [loading, isUpdating]); - if (loading) { + } catch (err) { + console.error("Error fetching poll data:", err); + openToast(gettext("Network error. Please check your connection.")); + } finally { + // 无论成功失败,只要请求完成就关闭全局 Loading + if (isLoading) { + setIsLoading(false); + } + } + }; + + // 优化点:进入页面立刻执行一次,避免 useInterval 产生 2 秒的白屏等待时间 + useEffect(() => { + fetchPollData(); + }, []); + + // 启动后续定时轮询 + useInterval(async () => { + await fetchPollData(); + }, pollDelay); + + if (isLoading) { return ; } + return ( <>
diff --git a/src/Components/Bootstrap/components/store.ts b/src/Components/Bootstrap/components/store.ts index ac09041..36c86ed 100644 --- a/src/Components/Bootstrap/components/store.ts +++ b/src/Components/Bootstrap/components/store.ts @@ -1,16 +1,16 @@ -import { configure, makeAutoObservable } from 'mobx'; -import type { BootstrapPollDataProps } from './typings.ts'; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { BootstrapPollDataModel } from "./types.ts"; -configure({ - enforceActions: 'observed', +type State = { + pollData: BootstrapPollDataModel | null; + setPollData: (pollData: BootstrapPollDataModel | null) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + setPollData: (data) => + set((state) => { + state.pollData = data; + }), }); -class Main { - pollData: BootstrapPollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (data: BootstrapPollDataProps | null) => { - this.pollData = data; - }; -} -export const BootstrapStore = new Main(); +export const useBootstrapStore = create()(immer(store)); diff --git a/src/Components/Bootstrap/components/typings.ts b/src/Components/Bootstrap/components/typings.ts deleted file mode 100644 index 96c55df..0000000 --- a/src/Components/Bootstrap/components/typings.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface BootstrapPollDataProps { - isDev: boolean; - version: string; - appName: string; - appUrl: string; - appConfigUrls: string[]; - appConfigUrlDev: string; - authorUrl: string; - authorName: string; - authorization: string; -} diff --git a/src/Components/BrowserBenchmark/components/browsers-item.module.scss b/src/Components/BrowserBenchmark/components/browsers-item.module.scss index 45c6d2d..742c9b3 100644 --- a/src/Components/BrowserBenchmark/components/browsers-item.module.scss +++ b/src/Components/BrowserBenchmark/components/browsers-item.module.scss @@ -2,11 +2,11 @@ --x-server-benchmark-bg: transparent; --x-server-benchmark-link-bg: hsl(0 0% 0% / 0.05); --x-server-benchmark-link-fg: hsl(0 0% 0% / 0.95); - @media (prefers-color-scheme: dark) { - // --x-server-benchmark-bg: hsl(0 0% 100% / 0.05); - --x-server-benchmark-link-fg: hsl(0 0% 100% / 0.95); - --x-server-benchmark-link-bg: hsl(0 0% 100% / 0.05); - } +} +:global([data-theme="dark"]) { + // --x-server-benchmark-bg: hsl(0 0% 100% / 0.05); + --x-server-benchmark-link-fg: hsl(0 0% 100% / 0.95); + --x-server-benchmark-link-bg: hsl(0 0% 100% / 0.05); } .main { display: grid; diff --git a/src/Components/BrowserBenchmark/components/browsers-item.tsx b/src/Components/BrowserBenchmark/components/browsers-item.tsx index f91b933..a259075 100644 --- a/src/Components/BrowserBenchmark/components/browsers-item.tsx +++ b/src/Components/BrowserBenchmark/components/browsers-item.tsx @@ -1,11 +1,11 @@ -import copyToClipboard from 'copy-to-clipboard'; -import type { FC, MouseEvent, ReactNode } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { template } from '@/Components/Utils/components/template.ts'; -import { UiRuby } from '@/Components/ui/ruby/index.tsx'; -import styles from './browsers-item.module.scss'; -import { BrowserBenchmarkMarksMeter } from './marks-meter.tsx'; -import type { BrowserBenchmarkMarksProps } from './typings.ts'; +import copyToClipboard from "copy-to-clipboard"; +import type { FC, MouseEvent, ReactNode } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { template } from "@/Components/Utils/components/template.ts"; +import { UiRuby } from "@/Components/ui/ruby/index.tsx"; +import styles from "./browsers-item.module.scss"; +import { BrowserBenchmarkMarksMeter } from "./marks-meter.tsx"; +import type { BrowserBenchmarkMarksProps } from "./types.ts"; const BrowserBenchmarkResult: FC<{ js: number; @@ -19,7 +19,7 @@ const BrowserBenchmarkResult: FC<{ const canvasString = canvas.toLocaleString(); const totalString = total.toLocaleString(); const totalText = template( - '{{js}} (JS) + {{dom}} (DOM) + {{canvas}} (Canvas) = {{total}}', + "{{js}} (JS) + {{dom}} (DOM) + {{canvas}} (Canvas) = {{total}}", { js: jsString, dom: domString, @@ -37,7 +37,7 @@ const BrowserBenchmarkResult: FC<{ ); }; diff --git a/src/Components/BrowserBenchmark/components/browsers.tsx b/src/Components/BrowserBenchmark/components/browsers.tsx index b14bffc..2b39396 100644 --- a/src/Components/BrowserBenchmark/components/browsers.tsx +++ b/src/Components/BrowserBenchmark/components/browsers.tsx @@ -1,75 +1,83 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, useEffect, useState } from 'react'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { Placeholder } from '@/Components/Placeholder/index.tsx'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { UiError } from '@/Components/ui/error/index.tsx'; -import { BrowserBenchmarkItem } from './browsers-item.tsx'; -import styles from './index.module.scss'; -import { BrowserBenchmarkMyBrowser } from './my-browser.tsx'; -import { BrowserBenchmarkStore } from './store.ts'; -import type { BrowserBenchmarkProps } from './typings.ts'; -export const BrowserBenchmarkBrowsers: FC = observer(() => { - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); +import { type FC, useEffect, useMemo, useState } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { Placeholder } from "@/Components/Placeholder/index.tsx"; +import { OK } from "@/Components/Rest/http-status.ts"; +import type { FetchStatus } from "@/Components/Utils/components/fetch-status.ts"; +import { UiError } from "@/Components/ui/error/index.tsx"; +import { BrowserBenchmarkItem } from "./browsers-item.tsx"; +import styles from "./index.module.scss"; +import { BrowserBenchmarkMyBrowser } from "./my-browser.tsx"; +import { useBrowserBenchmarkStore } from "./store.ts"; +import type { BrowserBenchmarkProps } from "./types.ts"; + +export const BrowserBenchmarkBrowsers: FC = () => { + const [status, setStatus] = useState("loading"); const { browsers, setBrowsers, setMaxMarks, maxMarks } = - BrowserBenchmarkStore; + useBrowserBenchmarkStore( + useShallow((s) => ({ + browsers: s.browsers, + maxMarks: s.maxMarks, + setBrowsers: s.setBrowsers, + setMaxMarks: s.setMaxMarks, + })), + ); useEffect(() => { const fetchData = async () => { - setLoading(true); - const { data, status } = - await serverFetch('browserBenchmarks'); - setLoading(false); - if (!data?.length || status !== OK) { - setError(true); + setStatus("loading"); + const { data, status: httpStatus } = await serverFetch< + BrowserBenchmarkProps[] + >("browserBenchmarks"); + if (!data?.length || httpStatus !== OK) { + setStatus("error"); return; } - setError(false); - let marks = 0; - setBrowsers( - data - .map((item) => { - item.total = item.detail - ? Object.values(item.detail).reduce((a, b) => a + b, 0) - : 0; - if (item.total > marks) { - marks = item.total; - } - return item; - }) - .toSorted((a, b) => (b?.total ?? 0) - (a?.total ?? 0)) - ); - setMaxMarks(marks); + const processedBrowsers = data.map((item) => ({ + ...item, + total: item.detail + ? Object.values(item.detail).reduce((a, b) => a + b, 0) + : 0, + })); + const sortedBrowsers = processedBrowsers + .slice() + .sort((a, b) => b.total - a.total); + const highestMark = sortedBrowsers[0]?.total ?? 0; + setBrowsers(sortedBrowsers); + setMaxMarks(highestMark); + setStatus("idel"); }; fetchData(); }, [setBrowsers, setMaxMarks]); - // const maxMarks = browsers.reduce((a, b) => Math.max(a, b?.total ?? 0), 0) - const results = browsers.map(({ name, version, ua, detail, date }) => { - if (!detail) { - return null; - } - const { js = 0, dom = 0, canvas = 0 } = detail; - return ( - - ); - }); + const results = useMemo( + () => + browsers + .filter((browser) => browser.detail) + .map(({ name, version, ua, detail, date }) => { + const { js = 0, dom = 0, canvas = 0 } = detail; + return ( + + ); + }), + [browsers, maxMarks], + ); return (
- {loading - ? [...new Array(5)].map(() => ) - : results} - {error && ( - {gettext('Can not fetch marks data from GitHub.')} - )} + {status === "loading" && + Array.from({ length: 5 }).map((_, i) => ( + + ))} + {status === "idel" && results} + {status === "error" && + {gettext("Can not fetch marks data from GitHub.")}}
); -}); +}; diff --git a/src/Components/BrowserBenchmark/components/constants.ts b/src/Components/BrowserBenchmark/components/constants.ts index 4375f71..3feaeb9 100644 --- a/src/Components/BrowserBenchmark/components/constants.ts +++ b/src/Components/BrowserBenchmark/components/constants.ts @@ -1,3 +1 @@ -export const BrowserBenchmarkConstants = { - id: 'browserBenchmark', -}; +export const BROWSER_BENCHMARK_ID = "browserBenchmark"; diff --git a/src/Components/BrowserBenchmark/components/index.tsx b/src/Components/BrowserBenchmark/components/index.tsx index 99380c2..2a6c52a 100644 --- a/src/Components/BrowserBenchmark/components/index.tsx +++ b/src/Components/BrowserBenchmark/components/index.tsx @@ -1,27 +1,22 @@ -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { UiDescription } from '@/Components/ui/description/index.tsx'; -import { BrowserBenchmarkBrowsers } from './browsers.tsx'; -import { BrowserBenchmarkConstants } from './constants.ts'; -import { BrowserBenchmarkMyBrowser } from './my-browser.tsx'; -export const BrowserBenchmark: FC = memo(() => { - return ( - - - - - ); -}); +import { type FC, memo } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { UiDescription } from "@/Components/ui/description/index.tsx"; +import { BrowserBenchmarkBrowsers } from "./browsers.tsx"; +import { BROWSER_BENCHMARK_ID } from "./constants.ts"; + +export const BrowserBenchmark: FC = memo(() => ( + + + + +)); diff --git a/src/Components/BrowserBenchmark/components/loader.ts b/src/Components/BrowserBenchmark/components/loader.ts index a623bf9..ff113bd 100644 --- a/src/Components/BrowserBenchmark/components/loader.ts +++ b/src/Components/BrowserBenchmark/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { BrowserBenchmarkConstants } from './constants.ts'; -import { BrowserBenchmark as content } from './index.tsx'; -import { BrowserBenchmarkNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { BROWSER_BENCHMARK_ID as id } from "./constants.ts"; +import { BrowserBenchmark as content } from "./index.tsx"; +import { BrowserBenchmarkNav as nav } from "./nav.tsx"; + export const BrowserBenchmarkLoader: ModuleProps = { - id: BrowserBenchmarkConstants.id, content, + id, nav, }; diff --git a/src/Components/BrowserBenchmark/components/marks-meter.tsx b/src/Components/BrowserBenchmark/components/marks-meter.tsx index e70d089..890074f 100644 --- a/src/Components/BrowserBenchmark/components/marks-meter.tsx +++ b/src/Components/BrowserBenchmark/components/marks-meter.tsx @@ -1,22 +1,20 @@ -import { MeterCore } from '@/Components/Meter/components/index.tsx'; -import styles from './marks-meter.module.scss'; +import { MeterCore } from "@/Components/Meter/components/index.tsx"; +import styles from "./marks-meter.module.scss"; + export const BrowserBenchmarkMarksMeter = ({ totalMarks, total, }: { totalMarks: number; total: number; -}) => { - return ( -
- -
- ); -}; +}) => ( +
+ +
+); diff --git a/src/Components/BrowserBenchmark/components/my-browser.tsx b/src/Components/BrowserBenchmark/components/my-browser.tsx index 697f9a8..a82c8e6 100644 --- a/src/Components/BrowserBenchmark/components/my-browser.tsx +++ b/src/Components/BrowserBenchmark/components/my-browser.tsx @@ -1,59 +1,66 @@ -import { observer } from 'mobx-react-lite'; -import { type MouseEvent, useCallback, useState } from 'react'; -import { Button } from '@/Components/Button/components/index.tsx'; -import { ButtonStatus } from '@/Components/Button/components/typings.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { BrowserBenchmarkItem } from './browsers-item.tsx'; -import { BrowserBenchmarkStore } from './store.ts'; -import { BrowserBenchmarkTests } from './tests.ts'; -import type { BrowserBenchmarkMarksProps } from './typings.ts'; -export const BrowserBenchmarkMyBrowser = observer(() => { - const [benchmarking, setBenchmarking] = useState(false); - const { setMaxMarks, maxMarks } = BrowserBenchmarkStore; +import { type MouseEvent, useCallback, useState } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Button } from "@/Components/Button/components/index.tsx"; +import { ButtonStatus } from "@/Components/Button/components/types.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { BrowserBenchmarkItem } from "./browsers-item.tsx"; +import { useBrowserBenchmarkStore } from "./store.ts"; +import { BrowserBenchmarkTests } from "./tests.ts"; +import type { BrowserBenchmarkMarksProps } from "./types.ts"; + +export const BrowserBenchmarkMyBrowser = () => { + const [isBenchmarking, setIsBenchmarking] = useState(false); + const { setMaxMarks, maxMarks } = useBrowserBenchmarkStore( + useShallow((s) => ({ + maxMarks: s.maxMarks, + setMaxMarks: s.setMaxMarks, + })), + ); const [marks, setMarks] = useState({ - js: 0, - dom: 0, canvas: 0, + dom: 0, + js: 0, }); const handleBenchmarking = useCallback( (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - if (benchmarking) { + if (isBenchmarking) { return; } if ( !window.confirm( gettext( - 'Running the benchmark may freeze the browser interface for a few seconds. Do you want to continue?' - ) + "Running the benchmark may freeze the browser interface for a few seconds. Do you want to continue?", + ), ) ) { return; } - setBenchmarking(true); + const tests = new BrowserBenchmarkTests(); + setIsBenchmarking(true); const results = { - js: BrowserBenchmarkTests.runJs(), - dom: BrowserBenchmarkTests.runDom(), - canvas: BrowserBenchmarkTests.runCanvas(), + canvas: tests.runCanvas(), + dom: tests.runDom(), + js: tests.runJs(), }; - setBenchmarking(false); + setIsBenchmarking(false); setMarks(results); const total = Object.values(results).reduce((a, b) => a + b, 0); if (total > maxMarks) { setMaxMarks(total); } }, - [benchmarking, maxMarks, setMaxMarks] + [isBenchmarking, maxMarks, setMaxMarks], ); const date = new Date(); const header = ( ); return ( @@ -64,4 +71,4 @@ export const BrowserBenchmarkMyBrowser = observer(() => { maxMarks={maxMarks} /> ); -}); +}; diff --git a/src/Components/BrowserBenchmark/components/nav.tsx b/src/Components/BrowserBenchmark/components/nav.tsx index 81a4534..16e6643 100644 --- a/src/Components/BrowserBenchmark/components/nav.tsx +++ b/src/Components/BrowserBenchmark/components/nav.tsx @@ -1,12 +1,8 @@ -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { BrowserBenchmarkConstants } from './constants.ts'; -export const BrowserBenchmarkNav: FC = () => { - return ( - - ); -}; +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { BROWSER_BENCHMARK_ID } from "./constants.ts"; + +export const BrowserBenchmarkNav: FC = () => ( + +); diff --git a/src/Components/BrowserBenchmark/components/store.ts b/src/Components/BrowserBenchmark/components/store.ts index 32e2d5b..3285da9 100644 --- a/src/Components/BrowserBenchmark/components/store.ts +++ b/src/Components/BrowserBenchmark/components/store.ts @@ -1,34 +1,42 @@ -import { configure, makeAutoObservable } from 'mobx'; -import type { BrowserBenchmarkProps } from './typings.ts'; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { BrowserBenchmarkProps } from "./types.ts"; -configure({ - enforceActions: 'observed', -}); -class Main { - benchmarking = false; - maxMarks = 0; - browsers: BrowserBenchmarkProps[] = []; - constructor() { - makeAutoObservable(this); - } - setMaxMarks = (maxMarks: number) => { - this.maxMarks = maxMarks; - }; - setBrowsers = (browsers: BrowserBenchmarkProps[]) => { - this.browsers = browsers; - }; - setBrowser = ( - id: BrowserBenchmarkProps['id'], +type State = { + benchmarking: boolean; + maxMarks: number; + browsers: BrowserBenchmarkProps[]; + setBenchmarking: (benchmarking: boolean) => void; + setMaxMarks: (maxMarks: number) => void; + setBrowsers: (browsers: BrowserBenchmarkProps[]) => void; + setBrowser: ( + id: BrowserBenchmarkProps["id"], item: BrowserBenchmarkProps - ) => { - const i = this.browsers.findIndex((n) => n.id === id); - if (i === -1) { - return; - } - this.browsers[i] = item; - }; - setBenchmarking = (benchmarking: boolean) => { - this.benchmarking = benchmarking; - }; -} -export const BrowserBenchmarkStore = new Main(); + ) => void; +}; +const store: StateCreator = (set) => ({ + benchmarking: false, + browsers: [], + maxMarks: 0, + setBenchmarking: (benchmarking) => + set((state) => { + state.benchmarking = benchmarking; + }), + setBrowser: (id, item) => + set((state) => { + const i = state.browsers.findIndex((n) => n.id === id); + if (i === -1) { + return; + } + state.browsers[i] = item; + }), + setBrowsers: (browsers) => + set((state) => { + state.browsers = browsers; + }), + setMaxMarks: (maxMarks) => + set((state) => { + state.maxMarks = maxMarks; + }), +}); +export const useBrowserBenchmarkStore = create()(immer(store)); diff --git a/src/Components/BrowserBenchmark/components/tests.ts b/src/Components/BrowserBenchmark/components/tests.ts index 82e1d5b..4955c97 100644 --- a/src/Components/BrowserBenchmark/components/tests.ts +++ b/src/Components/BrowserBenchmark/components/tests.ts @@ -1,13 +1,13 @@ -class Main { - getRndint = (min: number, max: number) => { - return Math.floor(Math.random() * (max - min + 1)) + min; - }; +export class BrowserBenchmarkTests { + getRndint = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; runJs = (): number => { const totalMs = 1000; let times = 0; const startTime = performance.now(); for (;;) { for (let i = 0; i < 10_000; i++) { + // biome-ignore lint/suspicious/noUnusedExpressions: (Math.sqrt(i) * Math.sin(i)) / Math.tan(i + 1); } const arr = new Array(1000).fill(0).map((_, i) => i); @@ -26,7 +26,7 @@ class Main { const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; const startTime = performance.now(); - const container = document.createElement('div'); + const container = document.createElement("div"); container.style.cssText = ` position: fixed; top: 0; @@ -37,22 +37,22 @@ height: 100%; document.body.appendChild(container); for (;;) { for (let i = 0; i < 100; i++) { - const div = document.createElement('div'); - div.className = 'benchmark-dom'; - div.style.position = 'fixed'; - div.style.left = '0px'; - div.style.top = '0px'; + const div = document.createElement("div"); + div.className = "benchmark-dom"; + div.style.position = "fixed"; + div.style.left = "0px"; + div.style.top = "0px"; div.style.width = `${this.getRndint(50, screenWidth)}px`; div.style.height = `${this.getRndint(50, screenHeight)}px`; - div.style.border = '1px solid green'; + div.style.border = "1px solid green"; container.appendChild(div); } // query const eles = document.querySelectorAll( - '.benchmark-dom' + ".benchmark-dom" ) as NodeListOf; for (const ele of Array.from(eles)) { - ele.style.borderColor = 'red'; + ele.style.borderColor = "red"; } while (container.firstChild) { container.removeChild(container.firstChild); @@ -68,16 +68,16 @@ height: 100%; runCanvas = () => { const totalMs = 1000; let times = 0; - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; canvas.width = screenWidth; canvas.height = screenHeight; - canvas.style.position = 'fixed'; - canvas.style.top = '0px'; - canvas.style.left = '0px'; + canvas.style.position = "fixed"; + canvas.style.top = "0px"; + canvas.style.left = "0px"; document.body.appendChild(canvas); - const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; const startTime = performance.now(); for (;;) { for (let i = 0; i < 100; i++) { @@ -86,8 +86,8 @@ height: 100%; const centerY = canvas.height / 2; const halfSize = canvas.width / 2; ctx.lineWidth = this.getRndint(1, 5); - ctx.strokeStyle = 'red'; - ctx.lineCap = 'round'; + ctx.strokeStyle = "red"; + ctx.lineCap = "round"; ctx.beginPath(); ctx.moveTo(centerX - halfSize, centerY - halfSize); ctx.lineTo(centerX + halfSize, centerY + halfSize); @@ -109,4 +109,3 @@ height: 100%; return times; }; } -export const BrowserBenchmarkTests = new Main(); diff --git a/src/Components/BrowserBenchmark/components/typings.ts b/src/Components/BrowserBenchmark/components/typings.ts deleted file mode 100644 index 377ceab..0000000 --- a/src/Components/BrowserBenchmark/components/typings.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface BrowserBenchmarkMarksProps { - js: number; - dom: number; - canvas: number; -} -export interface BrowserBenchmarkProps { - id: string; - name: string; - version: string; - ua: string; - date: string; - total: number; - detail: BrowserBenchmarkMarksProps; -} diff --git a/src/Components/Button/components/index.module.scss b/src/Components/Button/components/index.module.scss index 6348f4b..0cf47a7 100644 --- a/src/Components/Button/components/index.module.scss +++ b/src/Components/Button/components/index.module.scss @@ -5,14 +5,14 @@ --x-button-bg-hover: hsl(0 0% 0% / 0.15); --x-button-fg-active: var(--x-fg); --x-button-bg-active: hsl(0 0% 0% / 0.2); - @media (prefers-color-scheme: dark) { - --x-button-fg: var(--x-fg); - --x-button-bg: hsl(0 0% 100% / 0.1); - --x-button-fg-hover: var(--x-fg); - --x-button-bg-hover: hsl(0 0% 100% / 0.15); - --x-button-fg-active: var(--x-fg); - --x-button-bg-active: hsl(0 0% 100% / 0.2); - } +} +:global([data-theme="dark"]) { + --x-button-fg: var(--x-fg); + --x-button-bg: hsl(0 0% 100% / 0.1); + --x-button-fg-hover: var(--x-fg); + --x-button-bg-hover: hsl(0 0% 100% / 0.15); + --x-button-fg-active: var(--x-fg); + --x-button-bg-active: hsl(0 0% 100% / 0.2); } @keyframes spin { to { diff --git a/src/Components/Button/components/index.tsx b/src/Components/Button/components/index.tsx index a148079..8589ee9 100644 --- a/src/Components/Button/components/index.tsx +++ b/src/Components/Button/components/index.tsx @@ -1,45 +1,42 @@ -import { AlertTriangle, LoaderPinwheel, Pointer, X } from 'lucide-react'; -import type { AnchorHTMLAttributes, ButtonHTMLAttributes, FC } from 'react'; -import styles from './index.module.scss'; -import { ButtonStatus, type ButtonStatusValue } from './typings.ts';interface ButtonProps extends ButtonHTMLAttributes { +import { AlertTriangle, LoaderPinwheel, Pointer, X } from "lucide-react"; +import type { AnchorHTMLAttributes, ButtonHTMLAttributes, FC } from "react"; +import styles from "./index.module.scss"; +import { ButtonStatus, type ButtonStatusValue } from "./types.ts"; + +interface ButtonProps extends ButtonHTMLAttributes { status?: ButtonStatusValue; } interface LinkProps extends AnchorHTMLAttributes { status?: ButtonStatusValue; } -const Icon: FC<{ status: ButtonStatusValue }> = ({ status }) => { - return ( - - {{ - [ButtonStatus.Error]: , - [ButtonStatus.Loading]: , - [ButtonStatus.Warning]: , - [ButtonStatus.Pointer]: , - }?.[status] ?? null} - - ); -}; +const Icon: FC<{ status: ButtonStatusValue }> = ({ status }) => ( + + {{ + [ButtonStatus.Error]: , + [ButtonStatus.Loading]: , + [ButtonStatus.Warning]: , + [ButtonStatus.Pointer]: , + }[status] ?? null} + +); export const Button: FC = ({ status = ButtonStatus.Pointer, children, ...props -}) => { - return ( - - ); -}; +}) => ( + +); export const Link: FC = ({ status = ButtonStatus.Pointer, children, + href, ...props -}) => { - return ( - - - {children} - - ); -}; +}) => ( + + + {children} + +); diff --git a/src/Components/Button/components/typings.ts b/src/Components/Button/components/typings.ts deleted file mode 100644 index d272593..0000000 --- a/src/Components/Button/components/typings.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type ButtonStatusKey = 'Error' | 'Loading' | 'Warning' | 'Pointer'; -export type ButtonStatusValue = 'error' | 'loading' | 'warning' | 'pointer'; -export const ButtonStatus = { - Error: 'error', - Loading: 'loading', - Warning: 'warning', - Pointer: 'pointer', -} as const satisfies Record; diff --git a/src/Components/ColorScheme/components/config-dark.scss b/src/Components/ColorScheme/components/config-dark.scss index 587ae40..b875614 100644 --- a/src/Components/ColorScheme/components/config-dark.scss +++ b/src/Components/ColorScheme/components/config-dark.scss @@ -1,5 +1,5 @@ @mixin config { - @media (prefers-color-scheme: dark) { + :global([data-theme="dark"]) { :root { --x-fg: hsl(0, 0%, 80%); --x-bg: hsl(0, 0%, 0%); diff --git a/src/Components/ColorScheme/components/config.scss b/src/Components/ColorScheme/components/config.scss index e0038a1..444642e 100644 --- a/src/Components/ColorScheme/components/config.scss +++ b/src/Components/ColorScheme/components/config.scss @@ -5,8 +5,10 @@ --x-radius: 0.5rem; --x-fg: hsl(0, 0%, 20%); --x-bg: hsl(0, 0%, 97%); - --x-text-font-family: Verdana, Geneva, Tahoma, sans-serif; - --x-code-font-family: monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New"; + --x-text-font-family: "Noto Sans SC", Verdana, Geneva, Tahoma, sans-serif, "Microsoft YaHei UI"; + --x-code-font-family: + "Noto Sans Mono", monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", sans-serif, + "Microsoft YaHei UI"; --x-app-border-color: var(--x-fg); --x-app-bg: var(--x-bg); diff --git a/src/Components/Config/ConfigApi.php b/src/Components/Config/ConfigApi.php index bb865d8..a86741b 100644 --- a/src/Components/Config/ConfigApi.php +++ b/src/Components/Config/ConfigApi.php @@ -1,4 +1,5 @@ '9.1.0', - 'APP_NAME' => 'X Prober', - 'APP_URL' => 'https://github.com/kmvan/x-prober', - 'AUTHOR_URL' => 'https://inn-studio.com/prober', - 'UPDATE_PHP_URLS' => - array ( - 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/dist/prober.php', - 1 => 'https://api.inn-studio.com/download/?id=xprober', - ), - 'APP_CONFIG_URLS' => - array ( - 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/AppConfig.json', - 1 => 'https://api.inn-studio.com/download/?id=xprober-config', - ), - 'BENCHMARKS_URLS' => - array ( - 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/benchmarks.json', - 1 => 'https://api.inn-studio.com/download/?id=xprober-benchmarks', - ), - 'BROWSER_BENCHMARKS_URLS' => - array ( - 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/browser-benchmarks.json', - 1 => 'https://api.inn-studio.com/download/?id=xprober-browser-benchmarks', - ), - 'APP_CONFIG_URL_DEV' => 'http://localhost:8000/AppConfig.json', - 'APP_TEMPERATURE_SENSOR_URL' => 'http://127.0.0.1', - 'APP_TEMPERATURE_SENSOR_PORTS' => - array ( - 0 => 2048, - 1 => 4096, - ), - 'AUTHOR_NAME' => 'INN STUDIO', - 'LATEST_PHP_STABLE_VERSION' => '8', - 'LATEST_NGINX_STABLE_VERSION' => '1.22.0', - 'BENCHMARKS' => - array ( - 0 => - array ( - 'name' => 'Olink / E5-2680 v4 / PHP83 JIT', - 'url' => 'https://www.olink.cloud/clients/aff.php?aff=120', - 'date' => '2024-05-29', - 'proberUrl' => 'https://x-prober-server-benchmark-olink-sj.inn-studio.com', - 'binUrl' => '', - 'detail' => - array ( - 'cpu' => 268212, - 'read' => 18495, - 'write' => 6164, - ), - ), - 1 => - array ( - 'name' => 'RamNode / PHP82 JIT', - 'url' => 'https://clientarea.ramnode.com/aff.php?aff=4143', - 'date' => '2023-05-02', - 'detail' => - array ( - 'cpu' => 203245, - 'read' => 68706, - 'write' => 11452, - ), - ), - 2 => - array ( - 'name' => 'SpartanHost / HDD / PHP80 JIT', - 'url' => 'https://billing.spartanhost.net/aff.php?aff=801', - 'date' => '2021-07-17', - 'proberUrl' => 'https://x-prober-server-benchmark-spartanhost-dalls.inn-studio.com', - 'binUrl' => 'https://lg.dal.spartanhost.net/100MB.test', - 'detail' => - array ( - 'cpu' => 280903, - 'read' => 65551, - 'write' => 16238, - ), - ), - 3 => - array ( - 'name' => 'Vultr / Tokyo / PHP82 JIT', - 'url' => 'https://www.vultr.com/?ref=7826363-4F', - 'date' => '2023-05-02', - 'proberUrl' => 'https://x-prober-server-benchmark-vultr-tokyo.inn-studio.com/', - 'binUrl' => 'https://hnd-jp-ping.vultr.com/vultr.com.100MB.bin', - 'detail' => - array ( - 'cpu' => 243748, - 'read' => 46066, - 'write' => 13824, - ), - ), - 4 => - array ( - 'name' => 'BandwagonHOST / KVM / PHP80 JIT', - 'url' => 'https://bandwagonhost.com/aff.php?aff=34116', - 'proberUrl' => 'https://x-prober-server-benchmark-bwh-los-angeles.inn-studio.com/', - 'binUrl' => 'https://x-prober-server-benchmark-bwh-los-angeles.inn-studio.com/512m.bin', - 'date' => '2021-07-17', - 'detail' => - array ( - 'cpu' => 185491, - 'read' => 13616, - 'write' => 4529, - ), - ), - ), -); + public static $config = [ + 'APP_VERSION' => '9.1.1', + 'APP_NAME' => 'X Prober', + 'APP_URL' => 'https://github.com/kmvan/x-prober', + 'AUTHOR_URL' => 'https://inn-studio.com/prober', + 'UPDATE_PHP_URLS' => [ + 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/dist/prober.php', + 1 => 'https://api.inn-studio.com/download/?id=xprober', + ], + 'APP_CONFIG_URLS' => [ + 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/AppConfig.json', + 1 => 'https://api.inn-studio.com/download/?id=xprober-config', + ], + 'BENCHMARKS_URLS' => [ + 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/benchmarks.json', + 1 => 'https://api.inn-studio.com/download/?id=xprober-benchmarks', + ], + 'BROWSER_BENCHMARKS_URLS' => [ + 0 => 'https://raw.githubusercontent.com/kmvan/x-prober/master/browser-benchmarks.json', + 1 => 'https://api.inn-studio.com/download/?id=xprober-browser-benchmarks', + ], + 'APP_CONFIG_URL_DEV' => 'http://localhost:8000/AppConfig.json', + 'AUTHOR_NAME' => 'INN STUDIO', + 'LATEST_PHP_STABLE_VERSION' => '8', + 'LATEST_NGINX_STABLE_VERSION' => '1.22.0', + 'BENCHMARKS' => [ + 0 => [ + 'name' => 'Olink / E5-2680 v4 / PHP83 JIT', + 'url' => 'https://www.olink.cloud/clients/aff.php?aff=120', + 'date' => '2024-05-29', + 'proberUrl' => 'https://x-prober-server-benchmark-olink-sj.inn-studio.com', + 'binUrl' => '', + 'detail' => [ + 'cpu' => 268212, + 'read' => 18495, + 'write' => 6164, + ], + ], + 1 => [ + 'name' => 'RamNode / PHP82 JIT', + 'url' => 'https://clientarea.ramnode.com/aff.php?aff=4143', + 'date' => '2023-05-02', + 'detail' => [ + 'cpu' => 203245, + 'read' => 68706, + 'write' => 11452, + ], + ], + 2 => [ + 'name' => 'SpartanHost / HDD / PHP80 JIT', + 'url' => 'https://billing.spartanhost.net/aff.php?aff=801', + 'date' => '2021-07-17', + 'proberUrl' => 'https://x-prober-server-benchmark-spartanhost-dalls.inn-studio.com', + 'binUrl' => 'https://lg.dal.spartanhost.net/100MB.test', + 'detail' => [ + 'cpu' => 280903, + 'read' => 65551, + 'write' => 16238, + ], + ], + 3 => [ + 'name' => 'Vultr / Tokyo / PHP82 JIT', + 'url' => 'https://www.vultr.com/?ref=7826363-4F', + 'date' => '2023-05-02', + 'proberUrl' => 'https://x-prober-server-benchmark-vultr-tokyo.inn-studio.com/', + 'binUrl' => 'https://hnd-jp-ping.vultr.com/vultr.com.100MB.bin', + 'detail' => [ + 'cpu' => 243748, + 'read' => 46066, + 'write' => 13824, + ], + ], + 4 => [ + 'name' => 'BandwagonHOST / KVM / PHP80 JIT', + 'url' => 'https://bandwagonhost.com/aff.php?aff=34116', + 'proberUrl' => 'https://x-prober-server-benchmark-bwh-los-angeles.inn-studio.com/', + 'binUrl' => 'https://x-prober-server-benchmark-bwh-los-angeles.inn-studio.com/512m.bin', + 'date' => '2021-07-17', + 'detail' => [ + 'cpu' => 185491, + 'read' => 13616, + 'write' => 4529, + ], + ], + ], + ]; } diff --git a/src/Components/Config/ConfigPoll.php b/src/Components/Config/ConfigPoll.php index 4fe4618..2fc2074 100644 --- a/src/Components/Config/ConfigPoll.php +++ b/src/Components/Config/ConfigPoll.php @@ -17,8 +17,6 @@ final class ConfigPoll 'UPDATE_PHP_URLS' => $config['UPDATE_PHP_URLS'], 'APP_CONFIG_URLS' => $config['APP_CONFIG_URLS'], 'APP_CONFIG_URL_DEV' => $config['APP_CONFIG_URL_DEV'], - 'APP_TEMPERATURE_SENSOR_URL' => $config['APP_TEMPERATURE_SENSOR_URL'], - 'APP_TEMPERATURE_SENSOR_PORTS' => $config['APP_TEMPERATURE_SENSOR_PORTS'], 'AUTHOR_NAME' => $config['AUTHOR_NAME'], 'LATEST_PHP_STABLE_VERSION' => $config['LATEST_PHP_STABLE_VERSION'], 'LATEST_NGINX_STABLE_VERSION' => $config['LATEST_NGINX_STABLE_VERSION'], diff --git a/src/Components/Config/store.ts b/src/Components/Config/store.ts index c2a9d0e..f6ec0c0 100644 --- a/src/Components/Config/store.ts +++ b/src/Components/Config/store.ts @@ -1,18 +1,21 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '../Utils/components/is-deep-equal/index.ts'; -import type { ConfigProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { isDeepEqual } from "../Utils/components/is-deep-equal/index.ts"; +import type { ConfigProps } from "./types.ts"; + +type State = { + pollData: ConfigProps | null; + setPollData: (pollData: ConfigProps | null) => void; +}; +const createStore: StateCreator = (set) => ({ + pollData: null, + setPollData: (data) => { + set((state) => { + if (isDeepEqual(data, state.pollData)) { + return; + } + state.pollData = data; + }); + }, }); -class Main { - pollData: ConfigProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: ConfigProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; -} -export const ConfigStore = new Main(); +export const useConfigStore = create()(immer(createStore)); diff --git a/src/Components/Config/typings.ts b/src/Components/Config/typings.ts deleted file mode 100644 index 9824139..0000000 --- a/src/Components/Config/typings.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface ConfigProps { - APP_VERSION: string; - APP_NAME: string; - APP_URL: string; - AUTHOR_URL: string; - UPDATE_PHP_URLS: string[]; - APP_CONFIG_URLS: string[]; - BENCHMARKS_URLS: string[]; - APP_CONFIG_URL_DEV: string; - APP_TEMPERATURE_SENSOR_URL: string; - APP_TEMPERATURE_SENSOR_PORTS: number[]; - AUTHOR_NAME: string; - LATEST_PHP_STABLE_VERSION: string; - LATEST_NGINX_STABLE_VERSION: string; -} diff --git a/src/Components/Database/components/constants.ts b/src/Components/Database/components/constants.ts index e816c66..275ff39 100644 --- a/src/Components/Database/components/constants.ts +++ b/src/Components/Database/components/constants.ts @@ -1,3 +1 @@ -export const DatabaseConstants = { - id: 'database', -}; +export const DATABASE_ID = "database"; diff --git a/src/Components/Database/components/index.tsx b/src/Components/Database/components/index.tsx index e55acb3..2c460e7 100644 --- a/src/Components/Database/components/index.tsx +++ b/src/Components/Database/components/index.tsx @@ -1,35 +1,34 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { UiMultiColContainer } from '@/Components/ui/col/multi-container.tsx'; -import { EnableStatus } from '@/Components/ui/enable-status/index.tsx'; -import { DatabaseConstants } from './constants.ts'; -import { DatabaseStore } from './store'; -export const Database: FC = memo( - observer(() => { - const { pollData } = DatabaseStore; - const shortItems: [string, boolean | string][] = [ - ['SQLite3', pollData?.sqlite3 ?? false], - ['MySQLi client', pollData?.mysqliClientVersion ?? false], - ['Mongo', pollData?.mongo ?? false], - ['MongoDB', pollData?.mongoDb ?? false], - ['PostgreSQL', pollData?.postgreSql ?? false], - ['Paradox', pollData?.paradox ?? false], - ['MS SQL', pollData?.msSql ?? false], - ['PDO', pollData?.pdo ?? false], - ]; - return ( - - - {shortItems.map(([name, content]) => ( - - - - ))} - - - ); - }) -); +import { type FC, memo } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { UiMultiColContainer } from "@/Components/ui/col/multi-container.tsx"; +import { EnableStatus } from "@/Components/ui/enable-status/index.tsx"; +import { DATABASE_ID } from "./constants.ts"; +import { useDatabaseStore } from "./store"; + +export const Database: FC = memo(() => { + const pollData = useDatabaseStore(useShallow((s) => s.pollData)); + const shortItems: [string, boolean | string][] = [ + ["SQLite3", pollData?.sqlite3 ?? false], + ["MySQLi client", pollData?.mysqliClientVersion ?? false], + ["Mongo", pollData?.mongo ?? false], + ["MongoDB", pollData?.mongoDb ?? false], + ["PostgreSQL", pollData?.postgreSql ?? false], + ["Paradox", pollData?.paradox ?? false], + ["MS SQL", pollData?.msSql ?? false], + ["PDO", pollData?.pdo ?? false], + ]; + return ( + + + {shortItems.map(([name, content]) => ( + + + + ))} + + + ); +}); diff --git a/src/Components/Database/components/loader.ts b/src/Components/Database/components/loader.ts index 6b52bc8..882354c 100644 --- a/src/Components/Database/components/loader.ts +++ b/src/Components/Database/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { DatabaseConstants } from './constants.ts'; -import { Database as content } from './index.tsx'; -import { DatabaseNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { DATABASE_ID as id } from "./constants.ts"; +import { Database as content } from "./index.tsx"; +import { DatabaseNav as nav } from "./nav.tsx"; + export const DatabaseLoader: ModuleProps = { - id: DatabaseConstants.id, content, + id, nav, }; diff --git a/src/Components/Database/components/nav.tsx b/src/Components/Database/components/nav.tsx index 4a45fbd..7d724d7 100644 --- a/src/Components/Database/components/nav.tsx +++ b/src/Components/Database/components/nav.tsx @@ -1,6 +1,8 @@ -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { DatabaseConstants } from './constants.ts';export const DatabaseNav: FC = () => { - return ; -}; +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { DATABASE_ID } from "./constants.ts"; + +export const DatabaseNav: FC = () => ( + +); diff --git a/src/Components/Database/components/store.ts b/src/Components/Database/components/store.ts index b011022..8596039 100644 --- a/src/Components/Database/components/store.ts +++ b/src/Components/Database/components/store.ts @@ -1,18 +1,16 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { DatabasePollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { DatabasePollDataProps } from "./types.ts"; + +type State = { + pollData: DatabasePollDataProps | null; + setPollData: (pollData: DatabasePollDataProps | null) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + setPollData: (data) => + set((state) => { + state.pollData = data; + }), }); -class Main { - pollData: DatabasePollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: DatabasePollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; -} -export const DatabaseStore = new Main(); +export const useDatabaseStore = create()(immer(store)); diff --git a/src/Components/Database/components/typings.ts b/src/Components/Database/components/typings.ts deleted file mode 100644 index f57f905..0000000 --- a/src/Components/Database/components/typings.ts +++ /dev/null @@ -1 +0,0 @@ -export type DatabasePollDataProps = Record; diff --git a/src/Components/DiskUsage/components/constants.ts b/src/Components/DiskUsage/components/constants.ts index 814f4bb..f48e8e8 100644 --- a/src/Components/DiskUsage/components/constants.ts +++ b/src/Components/DiskUsage/components/constants.ts @@ -1,3 +1,4 @@ export const DiskUsageConstants = { - id: 'diskUsage', + id: "diskUsage", }; +export const DISK_USAGE_ID = "diskUsage"; diff --git a/src/Components/DiskUsage/components/index.tsx b/src/Components/DiskUsage/components/index.tsx index 9062c67..56dff6e 100644 --- a/src/Components/DiskUsage/components/index.tsx +++ b/src/Components/DiskUsage/components/index.tsx @@ -1,19 +1,19 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { DiskUsageConstants } from './constants.ts'; -import styles from './index.module.scss'; -import { DiskUsageStore } from './store.ts'; -export const DiskUsage: FC = observer(() => { - const { pollData } = DiskUsageStore; - const items = pollData?.items ?? []; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { DISK_USAGE_ID } from "./constants.ts"; +import styles from "./index.module.scss"; +import { useDiskUsageStore } from "./store.ts"; + +export const DiskUsage: FC = () => { + const items = useDiskUsageStore(useShallow((s) => s.pollData?.items ?? [])); if (!items.length) { return null; } return ( - +
{items.map(({ id, free, total }) => ( {
); -}); +}; diff --git a/src/Components/DiskUsage/components/loader.ts b/src/Components/DiskUsage/components/loader.ts index f01ff42..4926134 100644 --- a/src/Components/DiskUsage/components/loader.ts +++ b/src/Components/DiskUsage/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { DiskUsage as content } from '.'; -import { DiskUsageConstants } from './constants'; -import { DiskUsageNav as nav } from './nav'; +import type { ModuleProps } from "@/Components/Module/components/types"; +import { DiskUsage as content } from "."; +import { DISK_USAGE_ID as id } from "./constants"; +import { DiskUsageNav as nav } from "./nav"; + export const DiskUsageLoader: ModuleProps = { - id: DiskUsageConstants.id, content, + id, nav, }; diff --git a/src/Components/DiskUsage/components/nav.tsx b/src/Components/DiskUsage/components/nav.tsx index d34b7a1..5d0edd6 100644 --- a/src/Components/DiskUsage/components/nav.tsx +++ b/src/Components/DiskUsage/components/nav.tsx @@ -1,13 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { DiskUsageConstants } from './constants.ts'; -import { DiskUsageStore } from './store.ts';export const DiskUsageNav: FC = observer(() => { - const { pollData } = DiskUsageStore; - const items = pollData?.items ?? []; - if (!items.length) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { DISK_USAGE_ID } from "./constants.ts"; +import { useDiskUsageStore } from "./store.ts"; + +export const DiskUsageNav: FC = () => { + const hasItem = useDiskUsageStore((s) => Boolean(s.pollData?.items.length)); + if (!hasItem) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/DiskUsage/components/store.ts b/src/Components/DiskUsage/components/store.ts index 3715225..087b240 100644 --- a/src/Components/DiskUsage/components/store.ts +++ b/src/Components/DiskUsage/components/store.ts @@ -1,18 +1,16 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { DiskUsagePollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { DiskUsagePollDataProps } from "./types.ts"; + +type State = { + pollData: DiskUsagePollDataProps | null; + setPollData: (pollData: DiskUsagePollDataProps | null) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + setPollData: (pollData) => + set((state) => { + state.pollData = pollData; + }), }); -class Main { - pollData: DiskUsagePollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: DiskUsagePollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; -} -export const DiskUsageStore = new Main(); +export const useDiskUsageStore = create()(immer(store)); diff --git a/src/Components/DiskUsage/components/typings.ts b/src/Components/DiskUsage/components/typings.ts deleted file mode 100644 index e98d9fe..0000000 --- a/src/Components/DiskUsage/components/typings.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface DiskUsageItemProps { - id: string; - total: number; - free: number; -} -export interface DiskUsagePollDataProps { - items: DiskUsageItemProps[]; -} diff --git a/src/Components/Footer/components/index.module.scss b/src/Components/Footer/components/index.module.scss index f100db5..511361c 100644 --- a/src/Components/Footer/components/index.module.scss +++ b/src/Components/Footer/components/index.module.scss @@ -1,10 +1,10 @@ :root { --x-footer-bg: hsl(0 0% 0% / 0.05); --x-footer-fg: hsl(0 0% 0% / 0.5); - @media (prefers-color-scheme: dark) { - --x-footer-bg: hsl(0 0% 100% / 0.1); - --x-footer-fg: hsl(0 0% 100% / 0.5); - } +} +:global([data-theme="dark"]) { + --x-footer-bg: hsl(0 0% 100% / 0.1); + --x-footer-fg: hsl(0 0% 100% / 0.5); } .main { // background: var(--x-footer-bg); diff --git a/src/Components/Footer/components/index.tsx b/src/Components/Footer/components/index.tsx index 831ff59..94f5bd2 100644 --- a/src/Components/Footer/components/index.tsx +++ b/src/Components/Footer/components/index.tsx @@ -1,21 +1,22 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { PollStore } from '@/Components/Poll/components/store.ts'; -import { template } from '@/Components/Utils/components/template'; -import styles from './index.module.scss'; -export const Footer: FC = observer(() => { - const { pollData } = PollStore; - if (!pollData?.config) { +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { usePollStore } from "@/Components/Poll/components/store.ts"; +import { template } from "@/Components/Utils/components/template"; +import styles from "./index.module.scss"; + +export const Footer: FC = () => { + const config = usePollStore(useShallow((s) => s.pollData?.config)); + if (!config) { return null; } - const { APP_NAME, APP_URL, AUTHOR_NAME, AUTHOR_URL } = pollData.config; + const { APP_NAME, APP_URL, AUTHOR_NAME, AUTHOR_URL } = config; return (
${APP_NAME}`, authorName: `${AUTHOR_NAME}`, @@ -24,4 +25,4 @@ export const Footer: FC = observer(() => { }} /> ); -}); +}; diff --git a/src/Components/Header/components/bar.module.scss b/src/Components/Header/components/bar.module.scss index 966cc80..4a12194 100644 --- a/src/Components/Header/components/bar.module.scss +++ b/src/Components/Header/components/bar.module.scss @@ -2,11 +2,11 @@ --x-bar-fg: hsl(0 0% 0% / 0.5); --x-bar-bg-hover: hsl(0 0% 0% / 0.1); --x-bar-bg-active: hsl(0 0% 0% / 0.15); - @media (prefers-color-scheme: dark) { - --x-bar-fg: hsl(0 0% 100% / 0.5); - --x-bar-bg-hover: hsl(0 0% 100% / 0.1); - --x-bar-bg-active: hsl(0 0% 100% / 0.15); - } +} +:global([data-theme="dark"]) { + --x-bar-fg: hsl(0 0% 100% / 0.5); + --x-bar-bg-hover: hsl(0 0% 100% / 0.1); + --x-bar-bg-active: hsl(0 0% 100% / 0.15); } .main { display: grid; diff --git a/src/Components/Header/components/bar.tsx b/src/Components/Header/components/bar.tsx index 7b7bcea..6bd062e 100644 --- a/src/Components/Header/components/bar.tsx +++ b/src/Components/Header/components/bar.tsx @@ -1,9 +1,15 @@ -import { observer } from 'mobx-react-lite'; -import type { FC, MouseEvent } from 'react'; -import { NavStore } from '@/Components/Nav/components/store.ts'; -import styles from './bar.module.scss'; -export const HeaderBar: FC = observer(() => { - const { isOpen, setIsOpen } = NavStore; +import type { FC, MouseEvent } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { useNavStore } from "@/Components/Nav/components/store.ts"; +import styles from "./bar.module.scss"; + +export const HeaderBar: FC = () => { + const { isOpen, setIsOpen } = useNavStore( + useShallow((s) => ({ + isOpen: s.isOpen, + setIsOpen: s.setIsOpen, + })), + ); const handleToggleMenu = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -21,4 +27,4 @@ export const HeaderBar: FC = observer(() => { ); -}); +}; diff --git a/src/Components/Header/components/index.module.scss b/src/Components/Header/components/index.module.scss index dd8d861..e39de8e 100644 --- a/src/Components/Header/components/index.module.scss +++ b/src/Components/Header/components/index.module.scss @@ -3,12 +3,12 @@ --x-header-bg: transparent; --x-header-link-bg: hsl(0 0% 0% / 0.1); --x-header-link-bg-hover: hsl(0 0% 0% / 0.15); - @media (prefers-color-scheme: dark) { - --x-header-fg: hsl(0 0% 100% / 0.9); - --x-header-bg: hsl(0 0% 100% / 0.1); - --x-header-link-bg: hsl(0 0% 100% / 0.1); - --x-header-link-bg-hover: hsl(0 0% 100% / 0.15); - } +} +:global([data-theme="dark"]) { + --x-header-fg: hsl(0 0% 100% / 0.9); + --x-header-bg: hsl(0 0% 100% / 0.1); + --x-header-link-bg: hsl(0 0% 100% / 0.1); + --x-header-link-bg-hover: hsl(0 0% 100% / 0.15); } .main { display: flex; diff --git a/src/Components/Header/components/index.tsx b/src/Components/Header/components/index.tsx index daff200..72fc62f 100644 --- a/src/Components/Header/components/index.tsx +++ b/src/Components/Header/components/index.tsx @@ -1,10 +1,9 @@ -import type { FC } from 'react'; -import styles from './index.module.scss'; -import { HeaderName } from './name.tsx'; -export const Header: FC = () => { - return ( -
- -
- ); -}; +import type { FC } from "react"; +import styles from "./index.module.scss"; +import { HeaderName } from "./name.tsx"; + +export const Header: FC = () => ( +
+ +
+); diff --git a/src/Components/Header/components/link.module.scss b/src/Components/Header/components/link.module.scss index 8db0ce5..03f3060 100644 --- a/src/Components/Header/components/link.module.scss +++ b/src/Components/Header/components/link.module.scss @@ -3,12 +3,12 @@ --x-link-bg: hsl(0 0% 15% / 0.95); --x-link-bg-hover: hsl(0 0% 20% / 0.95); --x-link-bg-active: hsl(0 0% 25% / 0.95); - @media (prefers-color-scheme: dark) { - --x-link-fg: hsl(0 0% 100% / 0.95); - --x-link-bg: hsl(0 0% 10% / 0.95); - --x-link-bg-hover: hsl(0 0% 15% / 0.95); - --x-link-bg-active: hsl(0 0% 20% / 0.95); - } +} +:global([data-theme="dark"]) { + --x-link-fg: hsl(0 0% 100% / 0.95); + --x-link-bg: hsl(0 0% 10% / 0.95); + --x-link-bg-hover: hsl(0 0% 15% / 0.95); + --x-link-bg-active: hsl(0 0% 20% / 0.95); } .main { display: flex; diff --git a/src/Components/Header/components/name.tsx b/src/Components/Header/components/name.tsx index 874ddf8..853c0e4 100644 --- a/src/Components/Header/components/name.tsx +++ b/src/Components/Header/components/name.tsx @@ -1,46 +1,59 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, useEffect } from 'react'; -import { ConfigStore } from '@/Components/Config/store.ts'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { UpdaterStore } from '@/Components/Updater/components/store'; -import { UpdaterLink } from '@/Components/Updater/components/updater-link'; -import { versionCompare } from '@/Components/Utils/components/version-compare.ts'; -import { HeaderLink } from './link.tsx'; -import styles from './name.module.scss'; -export const HeaderName: FC = observer(() => { - const { pollData } = ConfigStore; - const { setTargetVersion, targetVersion } = UpdaterStore; +import { type FC, useEffect } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { useConfigStore } from "@/Components/Config/store.ts"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { useUpdaterStore } from "@/Components/Updater/components/store"; +import { UpdaterLink } from "@/Components/Updater/components/updater-link"; +import { versionCompare } from "@/Components/Utils/components/version-compare.ts"; +import { HeaderLink } from "./link.tsx"; +import styles from "./name.module.scss"; + +export const HeaderName: FC = () => { + const { hasPollData, APP_NAME, APP_URL, APP_VERSION } = useConfigStore( + useShallow((s) => ({ + APP_NAME: s.pollData?.APP_NAME, + APP_URL: s.pollData?.APP_URL, + APP_VERSION: s.pollData?.APP_VERSION, + hasPollData: Boolean(s.pollData), + })), + ); + const { setTargetVersion, targetVersion } = useUpdaterStore( + useShallow((s) => ({ + setTargetVersion: s.setTargetVersion, + targetVersion: s.targetVersion, + })), + ); // fetch new version useEffect(() => { - if (!pollData) { + if (!hasPollData) { return; } const fetchData = async () => { const { data, status } = await serverFetch<{ version: string; - }>('latestVersion'); + }>("latestVersion"); if (!data?.version || status !== OK) { return; } setTargetVersion(data.version); }; fetchData(); - }, [pollData, setTargetVersion]); - if (!pollData) { + }, [hasPollData, setTargetVersion]); + if (!hasPollData) { return null; } - const { APP_NAME, APP_URL, APP_VERSION } = pollData; return (

- {targetVersion && versionCompare(APP_VERSION, targetVersion) < 0 ? ( - - ) : ( - - {APP_NAME} - {APP_VERSION} - - )} + {targetVersion && APP_VERSION && + versionCompare(APP_VERSION, targetVersion) < 0 + ? + : ( + + {APP_NAME} + {APP_VERSION} + + )}

); -}); +}; diff --git a/src/Components/Location/components/index.tsx b/src/Components/Location/components/index.tsx index d54cde0..00e73cc 100644 --- a/src/Components/Location/components/index.tsx +++ b/src/Components/Location/components/index.tsx @@ -1,17 +1,18 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, type MouseEvent, useCallback, useState } from 'react'; -import { Button } from '@/Components/Button/components/index.tsx'; -import { ButtonStatus } from '@/Components/Button/components/typings.ts'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { ToastStore } from '@/Components/Toast/components/store.ts'; -import type { LocationProps } from './typings.ts'; +import { type FC, type MouseEvent, useCallback, useState } from "react"; +import { Button } from "@/Components/Button/components/index.tsx"; +import { ButtonStatus } from "@/Components/Button/components/types.ts"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { useToastStore } from "@/Components/Toast/components/store.ts"; +import type { LocationProps } from "./types.ts"; + export const Location: FC<{ ip: string; -}> = observer(({ ip }) => { +}> = ({ ip }) => { const [loading, setLoading] = useState(false); const [location, setLocation] = useState(null); + const open = useToastStore((s) => s.open); const onClick = useCallback( async (e: MouseEvent) => { e.preventDefault(); @@ -21,16 +22,16 @@ export const Location: FC<{ } setLoading(true); const { data, status } = await serverFetch( - `locationIpv4&ip=${ip}` + `locationIpv4&ip=${ip}`, ); setLoading(false); if (data && status === OK) { setLocation(data); return; } - ToastStore.open(gettext('Can not fetch location.')); + open(gettext("Can not fetch location.")); }, - [ip, loading] + [ip, loading, open], ); return ( ); -}); +}; diff --git a/src/Components/Location/components/typings.ts b/src/Components/Location/components/typings.ts deleted file mode 100644 index cce6794..0000000 --- a/src/Components/Location/components/typings.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface LocationProps { - continent: string; - country: string; - city: string; -} diff --git a/src/Components/Meter/components/index.module.scss b/src/Components/Meter/components/index.module.scss index 6f59b68..753edec 100644 --- a/src/Components/Meter/components/index.module.scss +++ b/src/Components/Meter/components/index.module.scss @@ -1,18 +1,18 @@ :root { - --x-meter-height: 2px; - // --x-meter-name-fg: hsl(0 0% 0% / 0.1); + --x-meter-height: 5px; + --x-meter-bg: oklch(0% 0 0 / 0.1); --x-meter-bar-bg: hsl(0 0% 0% / 0.1); --x-meter-value-bg: hsl(120 100% 40%); --x-meter-value-optimum-bg: hsl(120 100% 30%); --x-meter-value-suboptimum-bg: hsl(36 77% 64%); --x-meter-value-even-less-good-bg: hsl(12 100% 39%); - @media (prefers-color-scheme: dark) { - // --x-meter-name-fg: hsl(0 0% 100% / 0.9); - --x-meter-bar-bg: hsl(0 0% 100% / 0.1); - --x-meter-value-optimum-bg: hsl(120 100% 30%); - --x-meter-value-suboptimum-bg: hsl(36 77% 54%); - --x-meter-value-even-less-good-bg: hsl(12 100% 39%); - } +} +:global([data-theme="dark"]) { + --x-meter-bg: oklch(100% 0 0 / 0.1); + --x-meter-bar-bg: hsl(0 0% 100% / 0.1); + --x-meter-value-optimum-bg: hsl(120 100% 30%); + --x-meter-value-suboptimum-bg: hsl(36 77% 54%); + --x-meter-value-even-less-good-bg: hsl(12 100% 39%); } .main { display: grid; @@ -34,14 +34,15 @@ text-align: right; } .name { + all: unset; display: flex; grid-area: x-meter-name; align-items: center; border: none; background: none; color: var(--x-bg-fg); - font-weight: bold; - text-align: center; + // font-weight: bold; + // text-align: center; } .nameText { display: -webkit-box; @@ -55,7 +56,8 @@ } .core { grid-area: x-meter-core; - background: none; + border-radius: 10rem; + background: var(--x-meter-bg); width: 100%; height: var(--x-meter-height); &::-webkit-meter-bar { diff --git a/src/Components/Meter/components/index.tsx b/src/Components/Meter/components/index.tsx index 4a0e917..4547eb1 100644 --- a/src/Components/Meter/components/index.tsx +++ b/src/Components/Meter/components/index.tsx @@ -1,31 +1,30 @@ import { type FC, - type MouseEvent, memo, + type MouseEvent, type ReactNode, useCallback, -} from 'react'; -import { ToastStore } from '@/Components/Toast/components/store'; -import { formatBytes } from '@/Components/Utils/components/format-bytes'; -import styles from './index.module.scss'; +} from "react"; +import { useToastStore } from "@/Components/Toast/components/store.ts"; +import { formatBytes } from "@/Components/Utils/components/format-bytes"; +import styles from "./index.module.scss"; + export const MeterCore: FC<{ value: number; max?: number; low?: number; high?: number; optimum?: number; -}> = memo(({ value, max = 100, low = 60, optimum, high = 80 }) => { - return ( - - ); -}); +}> = memo(({ value, max = 100, low = 60, optimum, high = 80 }) => ( + +)); const MemoMeter: FC<{ title?: string; name?: string; @@ -40,27 +39,28 @@ const MemoMeter: FC<{ children?: ReactNode; }> = ({ title, - name = '', + name = "", value, max, isCapacity, - percentTag = '%', + percentTag = "%", percent, percentRender, progressPercent, }) => { + const open = useToastStore((s) => s.open); const handleNameClick = useCallback( (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); const content = title || name; - ToastStore.open(content); + open(content); if (title?.length ?? 0 >= 20) { return; } navigator.clipboard.writeText(name); }, - [name, title] + [name, title, open], ); const percentFallback = max === 0 || value === 0 ? 0 : (value / max) * 100; const overview = isCapacity diff --git a/src/Components/Module/components/arrow.module.scss b/src/Components/Module/components/arrow.module.scss index 4fee7e8..98fdb3d 100644 --- a/src/Components/Module/components/arrow.module.scss +++ b/src/Components/Module/components/arrow.module.scss @@ -2,11 +2,11 @@ --x-card-legend-arrow-fg: var(--x-card-legend-fg); --x-card-legend-arrow-bg-hover: hsl(0 0% 0% / 0.05); --x-card-legend-arrow-bg-active: hsl(0 0% 0% / 0.1); - @media (prefers-color-scheme: dark) { - --x-card-legend-arrow-fg: var(--x-card-legend-fg); - --x-card-legend-arrow-bg-hover: hsl(0 0% 100% / 0.05); - --x-card-legend-arrow-bg-active: hsl(0 0% 100% / 0.1); - } +} +:global([data-theme="dark"]) { + --x-card-legend-arrow-fg: var(--x-card-legend-fg); + --x-card-legend-arrow-bg-hover: hsl(0 0% 100% / 0.05); + --x-card-legend-arrow-bg-active: hsl(0 0% 100% / 0.1); } .arrow { display: flex; diff --git a/src/Components/Module/components/arrow.tsx b/src/Components/Module/components/arrow.tsx index 4a1fa69..d37b566 100644 --- a/src/Components/Module/components/arrow.tsx +++ b/src/Components/Module/components/arrow.tsx @@ -1,27 +1,43 @@ -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { observer } from 'mobx-react-lite'; -import { type FC, type MouseEvent, useCallback } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import styles from './arrow.module.scss'; -import { ModuleStore } from './store.ts'; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { type FC, type MouseEvent, useCallback } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import styles from "./arrow.module.scss"; +import { useModuleStore } from "./store.ts"; +import { useAvailableModules } from "./use-available-modules.ts"; + export const ModuleArrow: FC<{ isDown: boolean; - id: string; -}> = observer(({ isDown, id }) => { - const { disabledMoveUpId, disabledMoveDownId, moveUp, moveDown } = - ModuleStore; - const disabled = isDown ? disabledMoveDownId === id : disabledMoveUpId === id; + moduleId: string; +}> = ({ isDown, moduleId }) => { + // const pollData = usePollStore(useShallow((state) => state.pollData)); + const availableModules = useAvailableModules(); + const { moveUp, moveDown } = useModuleStore( + useShallow((s) => ({ + moveDown: s.moveDown, + moveUp: s.moveUp, + // priorities: s.priorities, + })), + ); + // const availablePriorities = pollData + // ? priorities.filter((n) => Object.hasOwn(pollData, n)) + // : []; + const isMoveDownDisabled = (id: string) => availableModules.at(-1)?.id === id; + const isMoveUpDisabled = (id: string) => availableModules.at(0)?.id === id; + const disabled = isDown + ? isMoveDownDisabled(moduleId) + : isMoveUpDisabled(moduleId); const handleMove = useCallback( (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (isDown) { - moveDown(id); + moveDown(moduleId); return; } - moveUp(id); + moveUp(moduleId); }, - [isDown, moveDown, moveUp, id] + [isDown, moveDown, moveUp, moduleId], ); return ( ); -}); +}; diff --git a/src/Components/Module/components/group.module.scss b/src/Components/Module/components/group.module.scss index 9fcd6f5..4e9ec9e 100644 --- a/src/Components/Module/components/group.module.scss +++ b/src/Components/Module/components/group.module.scss @@ -2,11 +2,11 @@ --x-card-group-label-fg: var(--x-fg); --x-card-group-split-color: hsl(0 0% 0% / 0.1); --x-card-group-bg-hover: hsl(0 0% 0% / 0.05); - @media (prefers-color-scheme: dark) { - --x-card-group-label-fg: var(--x-fg); - --x-card-group-split-color: hsl(0 0% 100% / 0.1); - --x-card-group-bg-hover: hsl(0 0% 100% / 0.05); - } +} +:global([data-theme="dark"]) { + --x-card-group-label-fg: var(--x-fg); + --x-card-group-split-color: hsl(0 0% 100% / 0.1); + --x-card-group-bg-hover: hsl(0 0% 100% / 0.05); } .main { display: grid; diff --git a/src/Components/Module/components/group.tsx b/src/Components/Module/components/group.tsx index c184cbc..428b0a0 100644 --- a/src/Components/Module/components/group.tsx +++ b/src/Components/Module/components/group.tsx @@ -1,15 +1,16 @@ -import type { CSSProperties, FC, ReactNode } from 'react'; -import styles from './group.module.scss'; +import type { CSSProperties, FC, ReactNode } from "react"; +import styles from "./group.module.scss"; + export const ModuleGroup: FC<{ label?: ReactNode; children: ReactNode; title?: string; minWidth?: number; maxWidth?: number; -}> = ({ label = '', title = '', minWidth = 4, maxWidth = 8, children }) => { +}> = ({ label = "", title = "", minWidth = 4, maxWidth = 8, children }) => { const style = { - '--min-width': `${minWidth}rem`, - '--max-width': `${maxWidth}rem`, + "--max-width": `${maxWidth}rem`, + "--min-width": `${minWidth}rem`, } as CSSProperties; return (
diff --git a/src/Components/Module/components/index.tsx b/src/Components/Module/components/index.tsx index fdfbb3e..5a2df70 100644 --- a/src/Components/Module/components/index.tsx +++ b/src/Components/Module/components/index.tsx @@ -1,34 +1,16 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, useEffect } from 'react'; -import { ModulePriority } from '@/Components/Module/components/priority.ts'; -import styles from './index.module.scss'; -import { ModulePreset } from './preset.ts'; -import { ModuleStorage } from './storage.ts'; -import { ModuleStore } from './store.ts'; -import type { SortedModuleProps } from './typings.ts'; -export const Modules: FC = observer(() => { - const { setSortedModules, availableModules } = ModuleStore; - useEffect(() => { - const storageItems = ModuleStorage.getItems(); - const sorted: SortedModuleProps[] = []; - for (const preset of ModulePreset.items) { - sorted.push({ - id: preset.id, - priority: - Number(storageItems?.[preset.id]) || - ModulePriority.indexOf(preset.id), - }); - } - setSortedModules(sorted); - }, [setSortedModules]); +import type { FC } from "react"; +import styles from "./index.module.scss"; +import { useAvailableModules } from "./use-available-modules.ts"; + +export const Modules: FC = () => { + const availableModules = useAvailableModules(); + // console.log("🚀 ~ Modules ~ availableModules:", availableModules); if (!availableModules.length) { return null; } return (
- {availableModules.map(({ id, content: C }) => { - return ; - })} + {availableModules.map(({ id, content: C }) => )}
); -}); +}; diff --git a/src/Components/Module/components/item.module.scss b/src/Components/Module/components/item.module.scss index 48965b0..0be89c4 100644 --- a/src/Components/Module/components/item.module.scss +++ b/src/Components/Module/components/item.module.scss @@ -6,17 +6,18 @@ --x-module-header-title-fg: hsl(0 0% 0% / 0.7); --x-module-header-title-bg: hsl(0 0% 0% / 0.1); --x-module-body-bg: var(--x-module-header-bg); - --x-module-box-shadow: hsla(0 0% 20% 0.3) 0px -1px 0px hsl(0 0% 100%) 0px 1px 0px inset, + --x-module-box-shadow: + hsla(0 0% 20% 0.3) 0px -1px 0px hsl(0 0% 100%) 0px 1px 0px inset, hsla(0 0% 20% 0.3) 0px -1px 0px inset hsl(0 0% 100%) 0px 1px 0px; - @media (prefers-color-scheme: dark) { - --x-module-bg: hsl(0 0% 15% / 0.95); - --x-module-header-bg: hsl(0 0% 100% / 0.1); - --x-module-header-fg: hsl(0 0% 100% / 0.7); - --x-module-header-title-fg: hsl(0 0% 100% / 0.7); - --x-module-header-title-bg: hsl(0 0% 100% / 0.1); - --x-module-body-bg: var(--x-module-header-bg); - --x-module-box-shadow: 0px 0px 0px 1px hsl(0 0% 0%) inset; - } +} +:global([data-theme="dark"]) { + --x-module-bg: hsl(0 0% 15% / 0.95); + --x-module-header-bg: hsl(0 0% 100% / 0.1); + --x-module-header-fg: hsl(0 0% 100% / 0.7); + --x-module-header-title-fg: hsl(0 0% 100% / 0.7); + --x-module-header-title-bg: hsl(0 0% 100% / 0.1); + --x-module-body-bg: var(--x-module-header-bg); + --x-module-box-shadow: 0px 0px 0px 1px hsl(0 0% 0%) inset; } .main { position: relative; diff --git a/src/Components/Module/components/item.tsx b/src/Components/Module/components/item.tsx index 3095901..47bca64 100644 --- a/src/Components/Module/components/item.tsx +++ b/src/Components/Module/components/item.tsx @@ -1,26 +1,24 @@ -import type { FC, ReactNode } from 'react'; -import { ModuleArrow } from '@/Components/Module/components/arrow.tsx'; -import styles from './item.module.scss';const ModuleItemTitle: FC<{ +import type { FC, ReactNode } from "react"; +import { ModuleArrow } from "@/Components/Module/components/arrow.tsx"; +import styles from "./item.module.scss"; + +const ModuleItemTitle: FC<{ id: string; title: string; -}> = ({ id, title }) => { - return ( -

- - {title} - -

- ); -}; +}> = ({ id, title }) => ( +

+ + {title} + +

+); export const ModuleItem: FC<{ id: string; title: string; children: ReactNode; -}> = ({ id, title, children, ...props }) => { - return ( -
- -
{children}
-
- ); -}; +}> = ({ id, title, children, ...props }) => ( +
+ +
{children}
+
+); diff --git a/src/Components/Module/components/preset.ts b/src/Components/Module/components/preset.ts index 3727482..998f3aa 100644 --- a/src/Components/Module/components/preset.ts +++ b/src/Components/Module/components/preset.ts @@ -1,30 +1,29 @@ -import { BrowserBenchmarkLoader } from '@/Components/BrowserBenchmark/components/loader.ts'; -import { DatabaseLoader } from '@/Components/Database/components/loader.ts'; -import { DiskUsageLoader } from '@/Components/DiskUsage/components/loader.ts'; -import { MyInfoLoader } from '@/Components/MyInfo/components/loader.ts'; -import { NetworkStatsLoader } from '@/Components/NetworkStats/components/loader.ts'; -import { NodesLoader } from '@/Components/Nodes/components/loader.ts'; -import { PhpExtensionsLoader } from '@/Components/PhpExtensions/components/loader.ts'; -import { PhpInfoLoader } from '@/Components/PhpInfo/components/loader.ts'; -import { PingLoader } from '@/Components/Ping/components/loader.ts'; -import { ServerBenchmarkLoader } from '@/Components/ServerBenchmark/components/loader.ts'; -import { ServerInfoLoader } from '@/Components/ServerInfo/components/loader.ts'; -import { ServerStatusLoader } from '@/Components/ServerStatus/components/loader.ts'; -import { TemperatureSensorLoader } from '@/Components/TemperatureSensor/components/loader.ts'; -export const ModulePreset = { - items: [ - NodesLoader, - TemperatureSensorLoader, - ServerStatusLoader, - NetworkStatsLoader, - DiskUsageLoader, - PingLoader, - ServerInfoLoader, - PhpInfoLoader, - PhpExtensionsLoader, - DatabaseLoader, - ServerBenchmarkLoader, - BrowserBenchmarkLoader, - MyInfoLoader, - ], -}; +import { BrowserBenchmarkLoader } from "@/Components/BrowserBenchmark/components/loader.ts"; +import { DatabaseLoader } from "@/Components/Database/components/loader.ts"; +import { DiskUsageLoader } from "@/Components/DiskUsage/components/loader.ts"; +import { MyInfoLoader } from "@/Components/MyInfo/components/loader.ts"; +import { NetworkStatsLoader } from "@/Components/NetworkStats/components/loader.ts"; +import { NodesLoader } from "@/Components/Nodes/components/loader.ts"; +import { PhpExtensionsLoader } from "@/Components/PhpExtensions/components/loader.ts"; +import { PhpInfoLoader } from "@/Components/PhpInfo/components/loader.ts"; +import { PingLoader } from "@/Components/Ping/components/loader.ts"; +import { ServerBenchmarkLoader } from "@/Components/ServerBenchmark/components/loader.ts"; +import { ServerInfoLoader } from "@/Components/ServerInfo/components/loader.ts"; +import { ServerStatusLoader } from "@/Components/ServerStatus/components/loader.ts"; +import { TemperatureSensorLoader } from "@/Components/TemperatureSensor/components/loader.ts"; + +export const presetModules = [ + NodesLoader, + TemperatureSensorLoader, + ServerStatusLoader, + NetworkStatsLoader, + DiskUsageLoader, + PingLoader, + ServerInfoLoader, + PhpInfoLoader, + PhpExtensionsLoader, + DatabaseLoader, + ServerBenchmarkLoader, + BrowserBenchmarkLoader, + MyInfoLoader, +]; diff --git a/src/Components/Module/components/priority.ts b/src/Components/Module/components/priority.ts index e5b605e..6a157a1 100644 --- a/src/Components/Module/components/priority.ts +++ b/src/Components/Module/components/priority.ts @@ -1,28 +1,47 @@ -import { BrowserBenchmarkConstants } from '@/Components/BrowserBenchmark/components/constants.ts'; -import { PingConstants } from '@/Components/Ping/components/constants.ts'; -import { DatabaseConstants } from '../../Database/components/constants.ts'; -import { DiskUsageConstants } from '../../DiskUsage/components/constants.ts'; -import { MyInfoConstants } from '../../MyInfo/components/constants.ts'; -import { NetworkStatsConstants } from '../../NetworkStats/components/constants.ts'; -import { NodesConstants } from '../../Nodes/components/constants.ts'; -import { PhpExtensionsConstants } from '../../PhpExtensions/components/constants.ts'; -import { PhpInfoConstants } from '../../PhpInfo/components/constants.ts'; -import { ServerBenchmarkConstants } from '../../ServerBenchmark/components/constants.ts'; -import { ServerInfoConstants } from '../../ServerInfo/components/constants.ts'; -import { ServerStatusConstants } from '../../ServerStatus/components/constants.ts'; -import { TemperatureSensorConstants } from '../../TemperatureSensor/components/constants.ts'; -export const ModulePriority = [ - NodesConstants.id, - TemperatureSensorConstants.id, - ServerStatusConstants.id, - NetworkStatsConstants.id, - DiskUsageConstants.id, - ServerInfoConstants.id, - PingConstants.id, - PhpInfoConstants.id, - PhpExtensionsConstants.id, - DatabaseConstants.id, - ServerBenchmarkConstants.id, - BrowserBenchmarkConstants.id, - MyInfoConstants.id, -]; +import { BROWSER_BENCHMARK_ID } from "@/Components/BrowserBenchmark/components/constants"; +import { DATABASE_ID } from "@/Components/Database/components/constants"; +import { DISK_USAGE_ID } from "@/Components/DiskUsage/components/constants"; +import { MY_INFO_ID } from "@/Components/MyInfo/components/constants"; +import { NETWORK_STATS_ID } from "@/Components/NetworkStats/components/constants"; +import { NODES_ID } from "@/Components/Nodes/components/constants"; +import { PHP_EXTENSIONS_ID } from "@/Components/PhpExtensions/components/constants"; +import { PHP_INFO_ID } from "@/Components/PhpInfo/components/constants"; +import { PING_ID } from "@/Components/Ping/components/constants"; +import { SERVER_BENCHMARK_ID } from "@/Components/ServerBenchmark/components/constants"; +import { SERVER_INFO_ID } from "@/Components/ServerInfo/components/constants"; +import { SERVER_STATUS_ID } from "@/Components/ServerStatus/components/constants"; +import { TEMPERATURE_SENSOR_ID } from "@/Components/TemperatureSensor/components/constants"; + +const STORAGE_KEY = "module-priority:v1"; +export const DEFAULT_MODULE_PRIORITES = [ + NODES_ID, + TEMPERATURE_SENSOR_ID, + SERVER_STATUS_ID, + NETWORK_STATS_ID, + DISK_USAGE_ID, + PING_ID, + SERVER_INFO_ID, + PHP_INFO_ID, + PHP_EXTENSIONS_ID, + DATABASE_ID, + SERVER_BENCHMARK_ID, + BROWSER_BENCHMARK_ID, + MY_INFO_ID, +] as const; +export const getStorageModulePriorities = (): string[] => { + const items = localStorage.getItem(STORAGE_KEY); + if (!items) { + return []; + } + try { + const data = JSON.parse(items); + return Array.isArray(data) ? data : []; + } catch { + return []; + } +}; +export const getStorageModulePriority = (id: string): number => + getStorageModulePriorities().indexOf(id); +export const setStorageModulePriorities = (ids: string[]) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)); +}; diff --git a/src/Components/Module/components/storage.ts b/src/Components/Module/components/storage.ts deleted file mode 100644 index 4866cd3..0000000 --- a/src/Components/Module/components/storage.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { StoragePriorityItemProps } from './store.ts';const STORAGE_KEY = 'module-priority'; -export const ModuleStorage = { - getItems(): Record { - const items = localStorage.getItem(STORAGE_KEY); - if (!items) { - return {}; - } - try { - return JSON.parse(items) as Record; - } catch { - return {}; - } - }, - setItems(items: Record) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); - }, - getPriority(id: string): number { - return this.getItems()[id] || 0; - }, - setPriority({ id, priority }: StoragePriorityItemProps) { - const items = this.getItems(); - items[id] = priority; - this.setItems(items); - }, -}; diff --git a/src/Components/Module/components/store.ts b/src/Components/Module/components/store.ts index 4a7a6f9..7a2fdd3 100644 --- a/src/Components/Module/components/store.ts +++ b/src/Components/Module/components/store.ts @@ -1,84 +1,59 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { ModulePriority } from '@/Components/Module/components/priority.ts'; -import { PollStore } from '@/Components/Poll/components/store.ts'; -import type { PollDataProps } from '@/Components/Poll/components/typings.ts'; -import { ModulePreset } from './preset.ts'; -import { ModuleStorage } from './storage.ts'; -import type { ModuleProps, SortedModuleProps } from './typings.ts'; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { + DEFAULT_MODULE_PRIORITES, + getStorageModulePriorities, + setStorageModulePriorities, +} from "@/Components/Module/components/priority.ts"; -configure({ - enforceActions: 'observed', -}); -export interface StoragePriorityItemProps { - id: string; - priority: number; -} -const saveSortedStorage = (items: SortedModuleProps[]) => { - const sorted: Record = {}; - for (const item of items) { - sorted[item.id] = item.priority; - } - ModuleStorage.setItems(sorted); +type State = { + priorities: string[]; + setPriorities: (ids: string[]) => void; + moveUp: (id: string) => void; + moveDown: (id: string) => void; }; -class Main { - sortedModules: SortedModuleProps[] = []; - constructor() { - makeAutoObservable(this); - } - setSortedModules = (modules: SortedModuleProps[]) => { - this.sortedModules = modules.toSorted((a, b) => { - return a.priority - b.priority; - }); - }; - get availableModules(): ModuleProps[] { - const { pollData } = PollStore; - const items = ModulePreset.items - .filter(({ id }) => Boolean(pollData?.[id as keyof PollDataProps])) - .toSorted((a, b) => { - const moduleA = this.sortedModules.find((item) => item.id === a.id); - const moduleB = this.sortedModules.find((item) => item.id === b.id); - return ( - Number(moduleA?.priority ?? ModulePriority.indexOf(a.id)) - - Number(moduleB?.priority ?? ModulePriority.indexOf(b.id)) - ); + +const store: StateCreator = (set) => { + const storagModulePriorities = getStorageModulePriorities(); + const initPriorities: string[] = DEFAULT_MODULE_PRIORITES.toSorted((a, b) => { + const index1 = storagModulePriorities.indexOf(a); + const index2 = storagModulePriorities.indexOf(b); + return (index1 < 0 ? 0 : index1) - (index2 < 0 ? 0 : index2); + }); + return { + moveDown: (id) => { + set((state) => { + const i = state.priorities.indexOf(id); + if (i < 0 || i === state.priorities.length - 1) { + return; + } + [state.priorities[i], state.priorities[i + 1]] = [ + state.priorities[i + 1], + state.priorities[i], + ]; + setStorageModulePriorities(state.priorities); }); - return items; - } - moveUp = (id: string) => { - const i = this.sortedModules.findIndex((item) => item.id === id); - if (i === 0) { - return; - } - const tmp = this.sortedModules[i].priority; - this.sortedModules[i].priority = this.sortedModules[i - 1].priority; - this.sortedModules[i - 1].priority = tmp; - this.sortedModules.sort((a, b) => a.priority - b.priority); - saveSortedStorage(this.sortedModules); + }, + moveUp: (id) => { + set((state) => { + const i = state.priorities.indexOf(id); + if (i <= 0) { + return; + } + [state.priorities[i], state.priorities[i - 1]] = [ + state.priorities[i - 1], + state.priorities[i], + ]; + setStorageModulePriorities(state.priorities); + }); + }, + priorities: initPriorities, + setPriorities: (ids) => + set((state) => { + state.priorities = ids; + setStorageModulePriorities(ids); + }), }; - moveDown = (id: string) => { - const i = this.sortedModules.findIndex((item) => item.id === id); - if (i === this.sortedModules.length - 1) { - return; - } - const tmp = this.sortedModules[i].priority; - this.sortedModules[i].priority = this.sortedModules[i + 1].priority; - this.sortedModules[i + 1].priority = tmp; - this.sortedModules.sort((a, b) => a.priority - b.priority); - saveSortedStorage(this.sortedModules); - }; - get disabledMoveUpId(): string { - const items = this.availableModules; - if (items.length <= 1) { - return ''; - } - return items[0].id; - } - get disabledMoveDownId(): string { - const items = this.availableModules; - if (items.length <= 1) { - return ''; - } - return items.at(-1)?.id ?? ''; - } -} -export const ModuleStore = new Main(); +}; + +export const useModuleStore = create()(immer(store)); diff --git a/src/Components/Module/components/typings.ts b/src/Components/Module/components/typings.ts deleted file mode 100644 index e03e853..0000000 --- a/src/Components/Module/components/typings.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { FC } from 'react';export interface ModuleProps { - id: string; - content: FC; - nav: FC; -} -export interface SortedModuleProps { - id: string; - priority: number; -} diff --git a/src/Components/MyInfo/components/constants.ts b/src/Components/MyInfo/components/constants.ts index c19594e..9613a9d 100644 --- a/src/Components/MyInfo/components/constants.ts +++ b/src/Components/MyInfo/components/constants.ts @@ -1,3 +1 @@ -export const MyInfoConstants = { - id: 'myInfo', -}; +export const MY_INFO_ID = "myInfo"; diff --git a/src/Components/MyInfo/components/index.tsx b/src/Components/MyInfo/components/index.tsx index f4923b8..7b42eba 100644 --- a/src/Components/MyInfo/components/index.tsx +++ b/src/Components/MyInfo/components/index.tsx @@ -1,25 +1,34 @@ -import { observer } from 'mobx-react-lite'; -import type { FC, ReactNode } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Location } from '@/Components/Location/components/index.tsx'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { useIp } from '@/Components/Utils/components/use-ip.ts'; -import { UiSingleColContainer } from '@/Components/ui/col/single-container.tsx'; -import { MyInfoConstants } from './constants.ts'; -import { MyInfoStore } from './store.ts'; -export const MyInfo: FC = observer(() => { - const { pollData } = MyInfoStore; +import type { FC, ReactNode } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Location } from "@/Components/Location/components/index.tsx"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { useIp } from "@/Components/Utils/components/use-ip.ts"; +import { UiSingleColContainer } from "@/Components/ui/col/single-container.tsx"; +import { MY_INFO_ID } from "./constants.ts"; +import { useMyInfoStore } from "./store.ts"; + +export const MyInfo: FC = () => { + const { hasPollData, pollDataIpv4, pollDataIpv6, phpLanguage } = + useMyInfoStore( + useShallow((s) => ({ + hasPollData: Boolean(s.pollData), + phpLanguage: s.pollData?.phpLanguage ?? "-", + pollDataIpv4: s.pollData?.ipv4 ?? "", + pollDataIpv6: s.pollData?.ipv6 ?? "", + })), + ); const { ip: ipv4, msg: ipv4Msg, isLoading: ipv4IsLoading } = useIp(4); const { ip: ipv6, msg: ipv6Msg, isLoading: ipv6IsLoading } = useIp(6); - let myIpv4 = ''; - let myIpv6 = ''; + let myIpv4 = ""; + let myIpv6 = ""; if (ipv4IsLoading) { myIpv4 = ipv4Msg; } else if (ipv4) { myIpv4 = ipv4; - } else if (pollData?.ipv4) { - myIpv4 = pollData.ipv4; + } else if (pollDataIpv4) { + myIpv4 = pollDataIpv4; } else { myIpv4 = ipv4Msg; } @@ -27,24 +36,24 @@ export const MyInfo: FC = observer(() => { myIpv6 = ipv6Msg; } else if (ipv6) { myIpv6 = ipv6; - } else if (pollData?.ipv6) { - myIpv6 = pollData.ipv6; + } else if (pollDataIpv6) { + myIpv6 = pollDataIpv6; } else { myIpv6 = ipv6Msg; } const items: [string, ReactNode][] = [ - [gettext('IPv4'), myIpv4], - [gettext('IPv6'), myIpv6], - [gettext('Location (IPv4)'), ], - [gettext('Browser UA'), navigator.userAgent], - [gettext('JS Browser languages'), navigator.languages.join(',')], - [gettext('PHP Browser languages'), pollData?.phpLanguage], + [gettext("IPv4"), myIpv4], + [gettext("IPv6"), myIpv6], + [gettext("Location (IPv4)"), ], + [gettext("Browser UA"), navigator.userAgent], + [gettext("JS Browser languages"), navigator.languages.join(",")], + [gettext("PHP Browser languages"), phpLanguage], ]; - if (!pollData) { + if (!hasPollData) { return null; } return ( - + {items.map(([name, content]) => ( @@ -54,4 +63,4 @@ export const MyInfo: FC = observer(() => { ); -}); +}; diff --git a/src/Components/MyInfo/components/loader.ts b/src/Components/MyInfo/components/loader.ts index 2bf977a..fe50408 100644 --- a/src/Components/MyInfo/components/loader.ts +++ b/src/Components/MyInfo/components/loader.ts @@ -1,9 +1,9 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { MyInfoConstants } from './constants.ts'; -import { MyInfo as content } from './index.tsx'; -import { MyInfoNav as nav } from './nav'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { MY_INFO_ID as id } from "./constants.ts"; +import { MyInfo as content } from "./index.tsx"; +import { MyInfoNav as nav } from "./nav"; export const MyInfoLoader: ModuleProps = { - id: MyInfoConstants.id, content, + id, nav, }; diff --git a/src/Components/MyInfo/components/nav.tsx b/src/Components/MyInfo/components/nav.tsx index eb3a1b6..a7cbbf9 100644 --- a/src/Components/MyInfo/components/nav.tsx +++ b/src/Components/MyInfo/components/nav.tsx @@ -1,12 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { MyInfoConstants } from './constants.ts'; -import { MyInfoStore } from './store.ts';export const MyInfoNav: FC = observer(() => { - const { pollData } = MyInfoStore; - if (!pollData) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { MY_INFO_ID } from "./constants.ts"; +import { useMyInfoStore } from "./store.ts"; + +export const MyInfoNav: FC = () => { + const hasPollData = useMyInfoStore((s) => Boolean(s.pollData)); + if (!hasPollData) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/MyInfo/components/store.ts b/src/Components/MyInfo/components/store.ts index 96c5324..9390f25 100644 --- a/src/Components/MyInfo/components/store.ts +++ b/src/Components/MyInfo/components/store.ts @@ -1,18 +1,16 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { MyInfoPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { MyInfoPollDataProps } from "./types.ts"; + +type State = { + pollData: MyInfoPollDataProps | null; + setPollData: (pollData: MyInfoPollDataProps | null) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + setPollData: (data) => + set((state) => { + state.pollData = data; + }), }); -class Main { - pollData: MyInfoPollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: MyInfoPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; -} -export const MyInfoStore = new Main(); +export const useMyInfoStore = create()(immer(store)); diff --git a/src/Components/MyInfo/components/typings.ts b/src/Components/MyInfo/components/typings.ts deleted file mode 100644 index 0a03230..0000000 --- a/src/Components/MyInfo/components/typings.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface MyInfoPollDataProps { - ipv4: string; - ipv6: string; - phpLanguage: string; -} diff --git a/src/Components/Nav/components/index.module.scss b/src/Components/Nav/components/index.module.scss index b5a93d5..839d99d 100644 --- a/src/Components/Nav/components/index.module.scss +++ b/src/Components/Nav/components/index.module.scss @@ -1,42 +1,62 @@ -@use "@/Components/Style/components//device.scss" as m; +@use "@/Components/Style/components/device.scss" as m; :root { - --x-nav-fg: hsl(0 0% 100% / 0.9); - --x-nav-bg: hsl(0 0% 15% / 0.95); - --x-nav-bg-hover: hsl(0 0% 100% / 0.05); - --x-nav-bg-active: hsl(0 0% 100% / 0.1); - --x-nav-border-color: hsl(0 0% 100% / 0.05); - @media (prefers-color-scheme: dark) { - --x-nav-fg: hsl(0 0% 95% / 0.95); - --x-nav-bg: hsl(0 0% 20% / 0.95); - --x-nav-bg-hover: hsl(0 0% 25% / 0.95); - --x-nav-bg-active: hsl(0 0% 30% / 0.95); - --x-nav-border-color: hsl(0 0% 100% / 0.05); + --x-nav-fg: oklch(0% 0 0 / 0.9); + --x-nav-bg: oklch(0% 0 0 / 0.1); + --x-nav-bg-hover: oklch(0% 0 0 / 0.1); + --x-nav-bg-active: oklch(0% 0 0 / 0.15); + --x-nav-border-color: linear-gradient(to bottom, transparent, oklch(0% 0 0 / 0.1), transparent); +} +:global([data-theme="dark"]) { + --x-nav-fg: oklch(100% 0 0 / 0.9); + --x-nav-bg: oklch(100% 0 0 / 0.15); + --x-nav-bg-hover: oklch(100% 0 0 / 0.1); + --x-nav-bg-active: oklch(100% 0 0 / 0.15); + --x-nav-border-color: linear-gradient(to bottom, transparent, oklch(100% 0 0 / 0.1), transparent); +} +.wrap { + display: flex; + position: sticky; + right: 0; + bottom: 0; + left: 0; + justify-content: center; + z-index: 10; + overflow: hidden; + @include m.device(lg) { + bottom: var(--x-gutter-sm); } } .main { display: flex; - position: sticky; - // right: 0; - bottom: 0; - // left: 0; - justify-content: flex-start; align-items: center; - z-index: 10; + backdrop-filter: blur(5px); background: var(--x-nav-bg); - // height: 3rem; overflow-x: auto; - // line-height: 3rem; @include m.device(lg) { - justify-content: center; - border-radius: var(--x-radius) var(--x-radius) 0 0; + border-radius: 10rem; + padding: 0 var(--x-gutter); } } .link { position: relative; - border-right: 1px solid var(--x-nav-border-color); + // border-right: 1px solid var(--x-nav-border-color); padding: var(--x-gutter); color: var(--x-nav-fg); white-space: nowrap; + &::after { + position: absolute; + top: 20%; + right: 0; + transform: translateY(-20%); + background: var(--x-nav-border-color); + width: 1px; + height: 80%; + pointer-events: none; + content: ""; + } + &:last-child::after { + display: none; + } &:hover { background: var(--x-nav-bg-hover); color: var(--x-nav-fg); @@ -49,7 +69,7 @@ color: var(--x-nav-fg); text-decoration: none; } - &:last-child { - border-right: 0; + @include m.device(lg) { + padding: var(--x-gutter-sm) var(--x-gutter); } } diff --git a/src/Components/Nav/components/index.tsx b/src/Components/Nav/components/index.tsx index 4f7a990..93cbc3b 100644 --- a/src/Components/Nav/components/index.tsx +++ b/src/Components/Nav/components/index.tsx @@ -1,18 +1,20 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { ModuleStore } from '@/Components/Module/components/store'; -import styles from './index.module.scss'; -export const Nav: FC = observer(() => { - const { availableModules } = ModuleStore; +import type { FC } from "react"; +import { useAvailableModules } from "@/Components/Module/components/use-available-modules"; +import styles from "./index.module.scss"; + +export const Nav: FC = () => { + const availableModules = useAvailableModules(); // const { activeIndex } = NavStore; - const items = availableModules.map(({ id, nav: Component }) => { - return ; - }); + const items = availableModules.map(({ id, nav: Component }) => ( + + )); // .filter((n) => n) as ReactElement[]; return ( -
- {items} - {/* {items} */} +
+
+ {items} + {/* {items} */} +
); -}); +}; diff --git a/src/Components/Nav/components/item.tsx b/src/Components/Nav/components/item.tsx index 3341554..8eee5da 100644 --- a/src/Components/Nav/components/item.tsx +++ b/src/Components/Nav/components/item.tsx @@ -1,12 +1,11 @@ -import type { FC } from 'react'; -import styles from './index.module.scss'; +import type { FC } from "react"; +import styles from "./index.module.scss"; + export const NavItem: FC<{ id: string; title: string; -}> = ({ id, title }) => { - return ( - - {title} - - ); -}; +}> = ({ id, title }) => ( + + {title} + +); diff --git a/src/Components/Nav/components/store.ts b/src/Components/Nav/components/store.ts index 52b4fd1..464f22d 100644 --- a/src/Components/Nav/components/store.ts +++ b/src/Components/Nav/components/store.ts @@ -1,17 +1,22 @@ -import { configure, makeAutoObservable } from 'mobx';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +type State = { + activeIndex: number; + isOpen: boolean; + setActiveIndex: (activeIndex: number) => void; + setIsOpen: (isOpen: boolean) => void; +}; +const store: StateCreator = (set) => ({ + activeIndex: 0, + isOpen: false, + setActiveIndex: (activeIndex) => + set((state) => { + state.activeIndex = activeIndex; + }), + setIsOpen: (isOpen) => + set((state) => { + state.isOpen = isOpen; + }), }); -class Main { - activeIndex = 0; - isOpen = false; - constructor() { - makeAutoObservable(this); - } - setActiveIndex = (activeIndex: typeof this.activeIndex) => { - this.activeIndex = activeIndex; - }; - setIsOpen = (isOpen: typeof this.isOpen) => { - this.isOpen = isOpen; - }; -} -export const NavStore = new Main(); +export const useNavStore = create()(immer(store)); diff --git a/src/Components/NetworkStats/components/constants.ts b/src/Components/NetworkStats/components/constants.ts index db9f71c..0b2a7dc 100644 --- a/src/Components/NetworkStats/components/constants.ts +++ b/src/Components/NetworkStats/components/constants.ts @@ -1,3 +1 @@ -export const NetworkStatsConstants = { - id: 'networkStats', -}; +export const NETWORK_STATS_ID = "networkStats"; diff --git a/src/Components/NetworkStats/components/index.tsx b/src/Components/NetworkStats/components/index.tsx index 40bd198..9c80e8c 100644 --- a/src/Components/NetworkStats/components/index.tsx +++ b/src/Components/NetworkStats/components/index.tsx @@ -1,40 +1,63 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { usePrevious } from '@/Components/Utils/components/use-previous.ts'; -import { NetworkStatsConstants } from './constants.ts'; -import styles from './index.module.scss'; -import { NetworksStatsItem } from './item'; -import { NetworkStatsStore } from './store'; -export const NetworkStats: FC = observer(() => { - const { sortNetworks, networksCount, timestamp } = NetworkStatsStore; +import { type FC, useMemo } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { usePrevious } from "@/Components/Utils/components/use-previous.ts"; +import { NETWORK_STATS_ID } from "./constants.ts"; +import styles from "./index.module.scss"; +import { NetworksStatsItem } from "./item"; +import { useNetworkStatsStore } from "./store"; + +export const NetworkStats: FC = () => { + const { networks, networksCount, timestamp } = useNetworkStatsStore( + useShallow((s) => ({ + networks: s.pollData?.networks ?? [], + networksCount: s.pollData?.networks.length, + timestamp: s.pollData?.timestamp ?? 0, + })), + ); + const sortNetworks = useMemo( + () => + networks.filter(({ tx }) => Boolean(tx)).toSorted((a, b) => a.tx - b.tx), + [networks], + ); const prevData = usePrevious({ items: sortNetworks, timestamp, }); + const prevItemsMap = useMemo(() => { + const map = new Map(); + const list = prevData?.items || sortNetworks; + for (const item of list) { + map.set(item.id, item); + } + return map; + }, [prevData?.items, sortNetworks]); if (!networksCount) { return null; } const seconds = timestamp - (prevData?.timestamp || timestamp); return ( - +
{sortNetworks.map(({ id, rx, tx }) => { if (!(rx || tx)) { return null; } - const prevItem = (prevData?.items || sortNetworks).find( - (item) => item.id === id - ); + // const prevItem = (prevData?.items || sortNetworks).find( + // (item) => item.id === id + // ); + const prevItem = prevItemsMap.get(id); const prevRx = prevItem?.rx || 0; const prevTx = prevItem?.tx || 0; + const rateRx = seconds > 0 ? (rx - prevRx) / seconds : 0; + const rateTx = seconds > 0 ? (tx - prevTx) / seconds : 0; return ( @@ -43,4 +66,4 @@ export const NetworkStats: FC = observer(() => {
); -}); +}; diff --git a/src/Components/NetworkStats/components/item.module.scss b/src/Components/NetworkStats/components/item.module.scss index 5a32123..9fea424 100644 --- a/src/Components/NetworkStats/components/item.module.scss +++ b/src/Components/NetworkStats/components/item.module.scss @@ -3,13 +3,12 @@ --x-network-stats-tx-bg: hsl(23 100% 38% / 0.1); --x-network-stats-rx-fg: hsl(120 100% 23%); --x-network-stats-rx-bg: hsl(120 100% 23% / 0.1); - - @media (prefers-color-scheme: dark) { - --x-network-stats-tx-fg: hsl(23 100% 58%); - --x-network-stats-tx-bg: hsl(23 100% 58% /0.15); - --x-network-stats-rx-fg: hsl(120 100% 43%); - --x-network-stats-rx-bg: hsl(120 100% 43% / 0.15); - } +} +:global([data-theme="dark"]) { + --x-network-stats-tx-fg: hsl(23 100% 58%); + --x-network-stats-tx-bg: hsl(23 100% 58% /0.15); + --x-network-stats-rx-fg: hsl(120 100% 43%); + --x-network-stats-rx-bg: hsl(120 100% 43% / 0.15); } .main { display: grid; diff --git a/src/Components/NetworkStats/components/loader.ts b/src/Components/NetworkStats/components/loader.ts index 88a2000..dfd9e6b 100644 --- a/src/Components/NetworkStats/components/loader.ts +++ b/src/Components/NetworkStats/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { NetworkStatsConstants } from './constants.ts'; -import { NetworkStats as content } from './index.tsx'; -import { NetworkStatsNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { NETWORK_STATS_ID as id } from "./constants.ts"; +import { NetworkStats as content } from "./index.tsx"; +import { NetworkStatsNav as nav } from "./nav.tsx"; + export const NetworkStatsLoader: ModuleProps = { - id: NetworkStatsConstants.id, content, + id, nav, }; diff --git a/src/Components/NetworkStats/components/nav.tsx b/src/Components/NetworkStats/components/nav.tsx index 8583c84..e263ca1 100644 --- a/src/Components/NetworkStats/components/nav.tsx +++ b/src/Components/NetworkStats/components/nav.tsx @@ -1,12 +1,15 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { NetworkStatsConstants } from './constants.ts'; -import { NetworkStatsStore } from './store.ts';export const NetworkStatsNav: FC = observer(() => { - const { networksCount } = NetworkStatsStore; - if (!networksCount) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { NETWORK_STATS_ID } from "./constants.ts"; +import { useNetworkStatsStore } from "./store.ts"; + +export const NetworkStatsNav: FC = () => { + const hasNetworks = useNetworkStatsStore((s) => + Boolean(s.pollData?.networks.length) + ); + if (!hasNetworks) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/NetworkStats/components/store.ts b/src/Components/NetworkStats/components/store.ts index ed8a4c6..730b6f9 100644 --- a/src/Components/NetworkStats/components/store.ts +++ b/src/Components/NetworkStats/components/store.ts @@ -1,31 +1,44 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { NetworkStatsPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', -});class Main { - pollData: NetworkStatsPollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData(pollData: NetworkStatsPollDataProps | null) { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - } - get networks(): NetworkStatsPollDataProps['networks'] { - return this.pollData?.networks ?? []; - } - get timestamp(): NetworkStatsPollDataProps['timestamp'] { - return this.pollData?.timestamp ?? 0; - } - get sortNetworks() { - return this.networks - .filter(({ tx }) => Boolean(tx)) - .toSorted((a, b) => a.tx - b.tx); - } - get networksCount() { - return this.sortNetworks.length; - } -} -export const NetworkStatsStore = new Main(); +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { NetworkStatsPollDataProps } from "./types.ts"; + +type State = { + pollData: NetworkStatsPollDataProps | null; + setPollData: (pollData: NetworkStatsPollDataProps | null) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + setPollData: (pollData) => + set((state) => { + state.pollData = pollData; + }), +}); +export const useNetworkStatsStore = create()(immer(store)); + +// class Main { +// pollData: NetworkStatsPollDataProps | null = null; +// constructor() { +// makeAutoObservable(this); +// } +// setPollData(pollData: NetworkStatsPollDataProps | null) { +// if (isDeepEqual(pollData, this.pollData)) { +// return; +// } +// this.pollData = pollData; +// } +// get networks(): NetworkStatsPollDataProps["networks"] { +// return this.pollData?.networks ?? []; +// } +// get timestamp(): NetworkStatsPollDataProps["timestamp"] { +// return this.pollData?.timestamp ?? 0; +// } +// get sortNetworks() { +// return this.networks +// .filter(({ tx }) => Boolean(tx)) +// .toSorted((a, b) => a.tx - b.tx); +// } +// get networksCount() { +// return this.sortNetworks.length; +// } +// } +// export const NetworkStatsStore = new Main(); diff --git a/src/Components/NetworkStats/components/typings.ts b/src/Components/NetworkStats/components/typings.ts deleted file mode 100644 index e7abf98..0000000 --- a/src/Components/NetworkStats/components/typings.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface NetworkStatsItemProps { - id: string; - rx: number; - tx: number; -} -export interface NetworkStatsPollDataProps { - networks: NetworkStatsItemProps[]; - timestamp: number; -} diff --git a/src/Components/Nodes/components/constants.ts b/src/Components/Nodes/components/constants.ts index 3be7456..358d50c 100644 --- a/src/Components/Nodes/components/constants.ts +++ b/src/Components/Nodes/components/constants.ts @@ -1,3 +1 @@ -export const NodesConstants = { - id: 'nodes', -}; +export const NODES_ID = "nodes"; diff --git a/src/Components/Nodes/components/cpu.tsx b/src/Components/Nodes/components/cpu.tsx index 15b7e10..063aa7b 100644 --- a/src/Components/Nodes/components/cpu.tsx +++ b/src/Components/Nodes/components/cpu.tsx @@ -1,24 +1,20 @@ -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { SysLoadItem } from '@/Components/ServerStatus/components/system-load.tsx'; -import type { ServerStatusPollDataProps } from '@/Components/ServerStatus/components/typings.ts'; -import styles from './cpu.module.scss'; -import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from './usage.tsx'; +import { type FC, memo } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { SysLoadItem } from "@/Components/ServerStatus/components/system-load.tsx"; +import type { ServerStatusPollDataProps } from "@/Components/ServerStatus/components/types.ts"; +import styles from "./cpu.module.scss"; +import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from "./usage.tsx"; const SysLoad: FC<{ items: number[]; -}> = ({ items }) => { - return ( -
- {items.map((n) => ( - - ))} -
- ); -}; +}> = ({ items }) => ( +
+ {items.map((n) => )} +
+); export const NodesCpu: FC<{ - sysLoad: ServerStatusPollDataProps['sysLoad']; - cpuUsage: ServerStatusPollDataProps['cpuUsage']; + sysLoad: ServerStatusPollDataProps["sysLoad"]; + cpuUsage: ServerStatusPollDataProps["cpuUsage"]; }> = memo(({ sysLoad, cpuUsage }) => { const { user, idle, sys, usage } = cpuUsage; const cpuTotal = user + idle + sys; @@ -29,7 +25,7 @@ sys: ${((sys / cpuTotal) * 100).toFixed(2)}% `; return ( - {gettext('CPU')} + {gettext("CPU")} diff --git a/src/Components/Nodes/components/disk.tsx b/src/Components/Nodes/components/disk.tsx index 18418d7..35a13aa 100644 --- a/src/Components/Nodes/components/disk.tsx +++ b/src/Components/Nodes/components/disk.tsx @@ -1,21 +1,19 @@ -import { type FC, memo } from 'react'; -import type { DiskUsageItemProps } from '@/Components/DiskUsage/components/typings.ts'; -import type { PollDataProps } from '@/Components/Poll/components/typings.ts'; -import { formatBytes } from '@/Components/Utils/components/format-bytes.ts'; -import styles from './disk.module.scss'; -import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from './usage.tsx';const Disk: FC = memo(({ id, free, total }) => { - return ( -
- - {`🖴 ${id}`} - {`${formatBytes(free)} / ${formatBytes(total)}`} - -
- ); -}); -export const NodesDisk: FC<{ data: PollDataProps['diskUsage'] }> = ({ - data, -}) => { +import { type FC, memo } from "react"; +import type { DiskUsageItemProps } from "@/Components/DiskUsage/components/types.ts"; +import type { PollData } from "@/Components/Poll/components/types.ts"; +import { formatBytes } from "@/Components/Utils/components/format-bytes.ts"; +import styles from "./disk.module.scss"; +import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from "./usage.tsx"; + +const Disk: FC = memo(({ id, free, total }) => ( +
+ + {`🖴 ${id}`} + {`${formatBytes(free)} / ${formatBytes(total)}`} + +
+)); +export const NodesDisk: FC<{ data: PollData["diskUsage"] }> = ({ data }) => { const items = data?.items ?? []; return (
diff --git a/src/Components/Nodes/components/index.tsx b/src/Components/Nodes/components/index.tsx index 7c2ce8d..a2350d0 100644 --- a/src/Components/Nodes/components/index.tsx +++ b/src/Components/Nodes/components/index.tsx @@ -1,24 +1,22 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { NodesConstants } from './constants.ts'; -import styles from './index.module.scss'; -import { Node } from './node.tsx'; -import { NodesStore } from './store'; -export const Nodes: FC = observer(() => { - const { pollData } = NodesStore; - const nodeIds = pollData?.nodesIds ?? []; - if (!nodeIds.length) { +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { NODES_ID } from "./constants.ts"; +import styles from "./index.module.scss"; +import { Node } from "./node.tsx"; +import { useNodesStore } from "./store"; + +export const Nodes: FC = () => { + const nodesIds = useNodesStore(useShallow((s) => s.pollData?.nodesIds ?? [])); + if (!nodesIds.length) { return null; } return ( - +
- {nodeIds.map((id) => ( - - ))} + {nodesIds.map((id) => )}
); -}); +}; diff --git a/src/Components/Nodes/components/loader.ts b/src/Components/Nodes/components/loader.ts index e4cf017..d2d77b9 100644 --- a/src/Components/Nodes/components/loader.ts +++ b/src/Components/Nodes/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { NodesConstants } from './constants.ts'; -import { Nodes as content } from './index.tsx'; -import { NodesNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { NODES_ID as id } from "./constants.ts"; +import { Nodes as content } from "./index.tsx"; +import { NodesNav as nav } from "./nav.tsx"; + export const NodesLoader: ModuleProps = { - id: NodesConstants.id, content, + id, nav, }; diff --git a/src/Components/Nodes/components/nav.tsx b/src/Components/Nodes/components/nav.tsx index 516864b..84db575 100644 --- a/src/Components/Nodes/components/nav.tsx +++ b/src/Components/Nodes/components/nav.tsx @@ -1,13 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { NodesConstants } from './constants.ts'; -import { NodesStore } from './store.ts';export const NodesNav: FC = observer(() => { - const { pollData } = NodesStore; - const nodeIds = pollData?.nodesIds ?? []; - if (!nodeIds.length) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { NODES_ID } from "./constants.ts"; +import { useNodesStore } from "./store.ts"; + +export const NodesNav: FC = () => { + const hasNodes = useNodesStore((s) => Boolean(s.pollData?.nodesIds.length)); + if (!hasNodes) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/Nodes/components/network.tsx b/src/Components/Nodes/components/network.tsx index 5414e13..e13fd79 100644 --- a/src/Components/Nodes/components/network.tsx +++ b/src/Components/Nodes/components/network.tsx @@ -1,8 +1,8 @@ -import type { FC } from 'react'; -import { NetworksStatsItem } from '@/Components/NetworkStats/components/item.tsx'; -import type { NetworkStatsPollDataProps } from '@/Components/NetworkStats/components/typings.ts'; -import { usePrevious } from '@/Components/Utils/components/use-previous.ts'; -import styles from './network.module.scss'; +import type { FC } from "react"; +import { NetworksStatsItem } from "@/Components/NetworkStats/components/item.tsx"; +import type { NetworkStatsPollDataProps } from "@/Components/NetworkStats/components/types"; +import { usePrevious } from "@/Components/Utils/components/use-previous.ts"; +import styles from "./network.module.scss"; export const NodesNetworkStats: FC<{ data: NetworkStatsPollDataProps }> = ({ data, }) => { diff --git a/src/Components/Nodes/components/node.tsx b/src/Components/Nodes/components/node.tsx index cca5ad6..49a32e2 100644 --- a/src/Components/Nodes/components/node.tsx +++ b/src/Components/Nodes/components/node.tsx @@ -1,49 +1,54 @@ -import { type FC, memo, useEffect, useState } from 'react'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { Placeholder } from '@/Components/Placeholder/index.tsx'; -import type { PollDataProps } from '@/Components/Poll/components/typings.ts'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { template } from '@/Components/Utils/components/template.ts'; -import { UiError } from '@/Components/ui/error/index.tsx'; -import { NodesCpu } from './cpu.tsx'; -import { NodesDisk } from './disk.tsx'; -import { NodesNetworkStats } from './network.tsx'; -import styles from './node.module.scss'; -import { NodesRam } from './ram.tsx'; -import { NodesSwap } from './swap.tsx'; -export const Node: FC<{ id: string }> = memo(({ id }) => { +import { type FC, useState } from "react"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { Placeholder } from "@/Components/Placeholder/index.tsx"; +import type { PollData } from "@/Components/Poll/components/types.ts"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { useUpdaterStore } from "@/Components/Updater/components/store.ts"; +import { template } from "@/Components/Utils/components/template.ts"; +import { useInterval } from "@/Components/Utils/components/use-interval.ts"; +import { UiError } from "@/Components/ui/error/index.tsx"; +import { NodesCpu } from "./cpu.tsx"; +import { NodesDisk } from "./disk.tsx"; +import { NodesNetworkStats } from "./network.tsx"; +import styles from "./node.module.scss"; +import { NodesRam } from "./ram.tsx"; +import { NodesSwap } from "./swap.tsx"; + +const TIMER = 2000; +export const Node: FC<{ id: string }> = ({ id }) => { const [loading, setLoading] = useState(true); + const isUpdating = useUpdaterStore((s) => s.isUpdating); const [error, setError] = useState(0); - const [pollData, setPollData] = useState(null); - useEffect(() => { - let timeoutId: NodeJS.Timeout; - let isMounted = true; - const fetchData = async () => { - try { - const { data, status } = await serverFetch( - `nodes&nodeId=${id}` - ); - if (loading) { - setLoading(false); - } - if (!data || status !== OK) { - setError(status); - return; - } - setPollData(data); - } finally { - if (isMounted) { - timeoutId = setTimeout(fetchData, 2000); - } + const [pollData, setPollData] = useState(null); + const pollDelay = isUpdating ? null : TIMER; + const fetchData = async () => { + try { + if (isUpdating) { + return; } - }; - fetchData(); - return () => { - isMounted = false; - clearTimeout(timeoutId); - }; - }, [id, loading]); + const { data, status } = await serverFetch( + `nodes&nodeId=${id}`, + ); + if (!data || status !== OK) { + setError(status); + } else { + setPollData(data); + } + if (loading) { + setLoading(false); + } + } catch (err) { + console.error("Error fetching node data:", err); + setError(-1); + } + }; + useInterval(async () => { + await fetchData(); + if (loading) { + setLoading(false); + } + }, pollDelay); const serverStatus = pollData?.serverStatus ?? null; const diskUsage = pollData?.diskUsage ?? null; const networkStats = pollData?.networkStats ?? null; @@ -55,7 +60,7 @@ export const Node: FC<{ id: string }> = memo(({ id }) => {
{id}
{error !== 0 && ( - {template(gettext('Error: {{error}}'), { error })} + {template(gettext("Error: {{error}}"), { error })} )} {loading && } {!loading && serverStatus && ( @@ -69,4 +74,4 @@ export const Node: FC<{ id: string }> = memo(({ id }) => { )}
); -}); +}; diff --git a/src/Components/Nodes/components/ram.tsx b/src/Components/Nodes/components/ram.tsx index f3603b6..adde5c7 100644 --- a/src/Components/Nodes/components/ram.tsx +++ b/src/Components/Nodes/components/ram.tsx @@ -1,17 +1,19 @@ -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import type { ServerStatusUsageProps } from '@/Components/ServerStatus/components/typings.ts'; -import { formatBytes } from '@/Components/Utils/components/format-bytes.ts'; -import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from './usage.tsx'; +import { type FC, memo } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import type { ServerStatusUsageProps } from "@/Components/ServerStatus/components/types.ts"; +import { formatBytes } from "@/Components/Utils/components/format-bytes.ts"; +import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from "./usage.tsx"; export const NodesRam: FC<{ data: ServerStatusUsageProps }> = memo( ({ data }) => { const { value, max } = data; const percent = max ? Math.round((value / max) * 100) : 0; return ( - {`🐏 ${gettext('Ram')}`} - {`${formatBytes(value)} / ${formatBytes(max)}`} + {`🐏 ${gettext("Ram")}`} + + {`${formatBytes(value)} / ${formatBytes(max)}`} + ); - } + }, ); diff --git a/src/Components/Nodes/components/store.ts b/src/Components/Nodes/components/store.ts index 80ac34e..a487c0c 100644 --- a/src/Components/Nodes/components/store.ts +++ b/src/Components/Nodes/components/store.ts @@ -1,37 +1,42 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { NodesItemProps, NodesPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { isDeepEqual } from "@/Components/Utils/components/is-deep-equal/index.ts"; +import type { NodesItemProps, NodesPollDataProps } from "./types.ts"; + +type State = { + items: NodesItemProps[]; + pollData: NodesPollDataProps | null; + setPollData: (pollData: NodesPollDataProps | null) => void; + setItems: (items: NodesItemProps[]) => void; + setItem: ({ id, ...props }: Partial) => void; +}; +// const DEFAULT_ITEM = { +// id: "", +// url: "", +// fetchUrl: "", +// loading: true, +// status: 204, +// data: null, +// }; +const createStore: StateCreator = (set) => ({ + items: [], + pollData: null, + setItem: ({ id, ...props }) => { + set((state) => { + const item = state.items.find((item) => item.id === id); + if (item) { + Object.assign(item, props); + } + }); + }, + setItems: (items) => set({ items }), + setPollData: (data) => { + set((state) => { + if (isDeepEqual(data, state.pollData)) { + return; + } + state.pollData = data; + }); + }, }); -class Main { - readonly DEFAULT_ITEM = { - id: '', - url: '', - fetchUrl: '', - loading: true, - status: 204, - data: null, - }; - items: NodesItemProps[] = []; - pollData: NodesPollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: NodesPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; - setItems = (items: NodesItemProps[]) => { - this.items = items; - }; - setItem = ({ id, ...props }: Partial) => { - const i = this.items.findIndex((item) => item.id === id); - if (i === -1) { - return; - } - this.items[i] = { ...this.items[i], ...props }; - }; -} -export const NodesStore = new Main(); +export const useNodesStore = create()(immer(createStore)); diff --git a/src/Components/Nodes/components/swap.tsx b/src/Components/Nodes/components/swap.tsx index 02fb8d7..6553a07 100644 --- a/src/Components/Nodes/components/swap.tsx +++ b/src/Components/Nodes/components/swap.tsx @@ -1,17 +1,19 @@ -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import type { ServerStatusUsageProps } from '@/Components/ServerStatus/components/typings.ts'; -import { formatBytes } from '@/Components/Utils/components/format-bytes.ts'; -import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from './usage.tsx'; +import { type FC, memo } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import type { ServerStatusUsageProps } from "@/Components/ServerStatus/components/types.ts"; +import { formatBytes } from "@/Components/Utils/components/format-bytes.ts"; +import { NodesUsage, NodesUsageLabel, NodesUsageOverview } from "./usage.tsx"; export const NodesSwap: FC<{ data: ServerStatusUsageProps }> = memo( ({ data }) => { const { value, max } = data; const percent = max ? Math.round((value / max) * 100) : 0; return ( - {`🐏 ${gettext('Swap')}`} - {`${formatBytes(value)} / ${formatBytes(max)}`} + {`🐏 ${gettext("Swap")}`} + + {`${formatBytes(value)} / ${formatBytes(max)}`} + ); - } + }, ); diff --git a/src/Components/Nodes/components/typings.ts b/src/Components/Nodes/components/typings.ts deleted file mode 100644 index f2c4e18..0000000 --- a/src/Components/Nodes/components/typings.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { PollDataProps } from '@/Components/Poll/components/typings.ts'; -export interface NodesItemProps { - id: string; - loading: boolean; - status: number; - data: PollDataProps | null; -} -export interface NodesPollDataProps { - nodesIds: string[]; -} diff --git a/src/Components/Nodes/components/usage.tsx b/src/Components/Nodes/components/usage.tsx index e8a1746..116b5b3 100644 --- a/src/Components/Nodes/components/usage.tsx +++ b/src/Components/Nodes/components/usage.tsx @@ -1,29 +1,28 @@ -import type { FC, HTMLProps, ReactNode } from 'react'; -import { MeterCore } from '@/Components/Meter/components/index.tsx'; -import styles from './usage.module.scss'; +import type { FC, HTMLProps, ReactNode } from "react"; +import { MeterCore } from "@/Components/Meter/components/index.tsx"; +import styles from "./usage.module.scss"; + export const NodesUsage: FC<{ children: ReactNode; percent: number }> = ({ children, percent, -}) => { - return ( -
- {children} -
{percent}%
-
- -
+}) => ( +
+ {children} +
{percent}%
+
+
- ); -}; -export const NodesUsageLabel: FC<{ children: ReactNode }> = (props) => { - return
; -}; -export const NodesUsageChart: FC<{ children: ReactNode }> = (props) => { - return
; -}; -export const NodesUsageOverview: FC> = (props) => { - return
; -}; +
+); +export const NodesUsageLabel: FC<{ children: ReactNode }> = (props) => ( +
+); +export const NodesUsageChart: FC<{ children: ReactNode }> = (props) => ( +
+); +export const NodesUsageOverview: FC> = (props) => ( +
+); // export const NodesUsagePercent: FC<{ percent: number }> = ({ percent }) => { // return
{percent}%
// } diff --git a/src/Components/PhpExtensions/components/constants.ts b/src/Components/PhpExtensions/components/constants.ts index ae962fa..cf0d372 100644 --- a/src/Components/PhpExtensions/components/constants.ts +++ b/src/Components/PhpExtensions/components/constants.ts @@ -1,3 +1 @@ -export const PhpExtensionsConstants = { - id: 'phpExtensions', -}; +export const PHP_EXTENSIONS_ID = "phpExtensions"; diff --git a/src/Components/PhpExtensions/components/index.tsx b/src/Components/PhpExtensions/components/index.tsx index 0ef279b..27db65d 100644 --- a/src/Components/PhpExtensions/components/index.tsx +++ b/src/Components/PhpExtensions/components/index.tsx @@ -1,95 +1,115 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { UiMultiColContainer } from '@/Components/ui/col/multi-container.tsx'; -import { UiSingleColContainer } from '@/Components/ui/col/single-container.tsx'; -import { EnableStatus } from '@/Components/ui/enable-status/index.tsx'; -import { SearchLink } from '@/Components/ui/search-link/index.tsx'; -import { PhpExtensionsConstants } from './constants.ts'; -import { PhpExtensionsStore } from './store.ts'; -export const PhpExtensions: FC = memo( - observer(() => { - const { pollData } = PhpExtensionsStore; - if (!pollData) { - return null; +import { type FC, memo, useMemo } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { UiMultiColContainer } from "@/Components/ui/col/multi-container.tsx"; +import { UiSingleColContainer } from "@/Components/ui/col/single-container.tsx"; +import { EnableStatus } from "@/Components/ui/enable-status/index.tsx"; +import { SearchLink } from "@/Components/ui/search-link/index.tsx"; +import { PHP_EXTENSIONS_ID } from "./constants.ts"; +import { usePhpExtensionsStore } from "./store.ts"; +export const PhpExtensions: FC = memo(() => { + const shortItemsRaw = usePhpExtensionsStore( + useShallow((s) => { + if (!s.pollData) { + return null; + } + return { + curl: s.pollData.curl, + exif: s.pollData.exif, + fileinfo: s.pollData.fileinfo, + gmagick: s.pollData.gmagick, + imagick: s.pollData.imagick, + ionCube: s.pollData.ionCube, + ldap: s.pollData.ldap, + mbstring: s.pollData.mbstring, + memcache: s.pollData.memcache, + memcached: s.pollData.memcached, + mysqli: s.pollData.mysqli, + opcache: s.pollData.opcache, + opcacheEnabled: s.pollData.opcacheEnabled, + opcacheJitEnabled: s.pollData.opcacheJitEnabled, + phalcon: s.pollData.phalcon, + redis: s.pollData.redis, + simplexml: s.pollData.simplexml, + sockets: s.pollData.sockets, + sourceGuardian: s.pollData.sourceGuardian, + sqlite3: s.pollData.sqlite3, + swoole: s.pollData.swoole, + xdebug: s.pollData.xdebug, + zendOptimizer: s.pollData.zendOptimizer, + zip: s.pollData.zip, + }; + }), + ); + const loadedExtensions = usePhpExtensionsStore( + useShallow((s) => s.pollData?.loadedExtensions), + ); + const sortedShortItems = useMemo(() => { + if (!shortItemsRaw) { + return []; } - const shortItems: [string, boolean][] = [ - ['Redis', Boolean(pollData.redis)], - ['SQLite3', Boolean(pollData.sqlite3)], - ['Memcache', Boolean(pollData.memcache)], - ['Memcached', Boolean(pollData.memcached)], - ['Opcache', Boolean(pollData.opcache)], - [gettext('Opcache enabled'), Boolean(pollData.opcacheEnabled)], - [gettext('Opcache JIT enabled'), Boolean(pollData.opcacheJitEnabled)], - ['Swoole', Boolean(pollData.swoole)], - ['Image Magick', Boolean(pollData.imagick)], - ['Graphics Magick', Boolean(pollData.gmagick)], - ['Exif', Boolean(pollData.exif)], - ['Fileinfo', Boolean(pollData.fileinfo)], - ['SimpleXML', Boolean(pollData.simplexml)], - ['Sockets', Boolean(pollData.sockets)], - ['MySQLi', Boolean(pollData.mysqli)], - ['Zip', Boolean(pollData.zip)], - ['Multibyte String', Boolean(pollData.mbstring)], - ['Phalcon', Boolean(pollData.phalcon)], - ['Xdebug', Boolean(pollData.xdebug)], - ['Zend Optimizer', Boolean(pollData.zendOptimizer)], - ['ionCube', Boolean(pollData.ionCube)], - ['Source Guardian', Boolean(pollData.sourceGuardian)], - ['LDAP', Boolean(pollData.ldap)], - ['cURL', Boolean(pollData.curl)], + const items: [string, boolean][] = [ + ["Redis", Boolean(shortItemsRaw.redis)], + ["SQLite3", Boolean(shortItemsRaw.sqlite3)], + ["Memcache", Boolean(shortItemsRaw.memcache)], + ["Memcached", Boolean(shortItemsRaw.memcached)], + ["Opcache", Boolean(shortItemsRaw.opcache)], + [gettext("Opcache enabled"), Boolean(shortItemsRaw.opcacheEnabled)], + [ + gettext("Opcache JIT enabled"), + Boolean(shortItemsRaw.opcacheJitEnabled), + ], + ["Swoole", Boolean(shortItemsRaw.swoole)], + ["Image Magick", Boolean(shortItemsRaw.imagick)], + ["Graphics Magick", Boolean(shortItemsRaw.gmagick)], + ["Exif", Boolean(shortItemsRaw.exif)], + ["Fileinfo", Boolean(shortItemsRaw.fileinfo)], + ["SimpleXML", Boolean(shortItemsRaw.simplexml)], + ["Sockets", Boolean(shortItemsRaw.sockets)], + ["MySQLi", Boolean(shortItemsRaw.mysqli)], + ["Zip", Boolean(shortItemsRaw.zip)], + ["Multibyte String", Boolean(shortItemsRaw.mbstring)], + ["Phalcon", Boolean(shortItemsRaw.phalcon)], + ["Xdebug", Boolean(shortItemsRaw.xdebug)], + ["Zend Optimizer", Boolean(shortItemsRaw.zendOptimizer)], + ["ionCube", Boolean(shortItemsRaw.ionCube)], + ["Source Guardian", Boolean(shortItemsRaw.sourceGuardian)], + ["LDAP", Boolean(shortItemsRaw.ldap)], + ["cURL", Boolean(shortItemsRaw.curl)], ]; - shortItems.slice().sort((a, b) => { - const x = a[0].toLowerCase(); - const y = b[0].toLowerCase(); - if (x < y) { - return -1; - } - if (x > y) { - return 1; - } - return 0; - }); - const longItems: string[] = pollData.loadedExtensions || []; - longItems.slice().sort((a, b) => { - const x = a.toLowerCase(); - const y = b.toLowerCase(); - if (x < y) { - return -1; - } - if (x > y) { - return 1; - } - return 0; - }); - return ( - - - {shortItems.map(([name, enabled]) => ( - - - - ))} - - - {Boolean(longItems.length) && ( - - {longItems.map((id) => ( - - ))} - - )} - - - ); - }) -); + return items.sort((a, b) => a[0].localeCompare(b[0])); + }, [shortItemsRaw]); + const sortedLongItems = useMemo(() => { + if (!loadedExtensions) { + return []; + } + return loadedExtensions.slice().sort((a, b) => a.localeCompare(b)); + }, [loadedExtensions]); + if (!shortItemsRaw) { + return null; + } + return ( + + + {sortedShortItems.map(([name, enabled]) => ( + + + + ))} + + + {Boolean(sortedLongItems.length) && ( + + {sortedLongItems.map((id) => )} + + )} + + + ); +}); diff --git a/src/Components/PhpExtensions/components/loader.ts b/src/Components/PhpExtensions/components/loader.ts index 734c551..c58b57a 100644 --- a/src/Components/PhpExtensions/components/loader.ts +++ b/src/Components/PhpExtensions/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { PhpExtensionsConstants } from './constants.ts'; -import { PhpExtensions as content } from './index.tsx'; -import { PhpExtensionsNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { PHP_EXTENSIONS_ID as id } from "./constants.ts"; +import { PhpExtensions as content } from "./index.tsx"; +import { PhpExtensionsNav as nav } from "./nav.tsx"; + export const PhpExtensionsLoader: ModuleProps = { - id: PhpExtensionsConstants.id, content, + id, nav, }; diff --git a/src/Components/PhpExtensions/components/nav.tsx b/src/Components/PhpExtensions/components/nav.tsx index 8876224..6bfbfcd 100644 --- a/src/Components/PhpExtensions/components/nav.tsx +++ b/src/Components/PhpExtensions/components/nav.tsx @@ -1,13 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { PhpExtensionsConstants } from './constants.ts'; -import { PhpExtensionsStore } from './store.ts'; -export const PhpExtensionsNav: FC = observer(() => { - const { pollData } = PhpExtensionsStore; - if (!pollData) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { PHP_EXTENSIONS_ID } from "./constants.ts"; +import { usePhpExtensionsStore } from "./store.ts"; + +export const PhpExtensionsNav: FC = () => { + const hasPollData = usePhpExtensionsStore((s) => Boolean(s.pollData)); + if (!hasPollData) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/PhpExtensions/components/store.ts b/src/Components/PhpExtensions/components/store.ts index 8e1def9..380a576 100644 --- a/src/Components/PhpExtensions/components/store.ts +++ b/src/Components/PhpExtensions/components/store.ts @@ -1,18 +1,17 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { PhpExtensionsPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { PhpExtensionsPollDataProps } from "./types.ts"; + +type State = { + pollData: PhpExtensionsPollDataProps | null; + setPollData: (pollData: PhpExtensionsPollDataProps | null) => void; +}; +const store: StateCreator = (set) => ({ + latestPhpVersion: "", + pollData: null, + setPollData: (data) => + set((state) => { + state.pollData = data; + }), }); -class Main { - pollData: PhpExtensionsPollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: PhpExtensionsPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; -} -export const PhpExtensionsStore = new Main(); +export const usePhpExtensionsStore = create()(immer(store)); diff --git a/src/Components/PhpExtensions/components/typings.ts b/src/Components/PhpExtensions/components/typings.ts deleted file mode 100644 index f1e9f4e..0000000 --- a/src/Components/PhpExtensions/components/typings.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface PhpExtensionsPollDataProps { - redis: boolean; - sqlite3: boolean; - memcache: boolean; - memcached: boolean; - opcache: boolean; - opcacheEnabled: boolean; - opcacheJitEnabled: boolean; - swoole: boolean; - imagick: boolean; - gmagick: boolean; - exif: boolean; - fileinfo: boolean; - simplexml: boolean; - sockets: boolean; - mysqli: boolean; - zip: boolean; - mbstring: boolean; - phalcon: boolean; - xdebug: boolean; - zendOptimizer: boolean; - ionCube: boolean; - sourceGuardian: boolean; - ldap: boolean; - curl: boolean; - loadedExtensions: string[]; -} diff --git a/src/Components/PhpInfo/components/constants.ts b/src/Components/PhpInfo/components/constants.ts index f62d3dc..282b313 100644 --- a/src/Components/PhpInfo/components/constants.ts +++ b/src/Components/PhpInfo/components/constants.ts @@ -1,3 +1 @@ -export const PhpInfoConstants = { - id: 'phpInfo', -}; +export const PHP_INFO_ID = "phpInfo"; diff --git a/src/Components/PhpInfo/components/index.tsx b/src/Components/PhpInfo/components/index.tsx index bbd064f..a3471c0 100644 --- a/src/Components/PhpInfo/components/index.tsx +++ b/src/Components/PhpInfo/components/index.tsx @@ -1,101 +1,100 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, memo, type ReactNode } from 'react'; -import { Link } from '@/Components/Button/components/index.tsx'; -import { serverFetchRoute } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { UiMultiColContainer } from '@/Components/ui/col/multi-container.tsx'; -import { UiSingleColContainer } from '@/Components/ui/col/single-container.tsx'; -import { EnableStatus } from '@/Components/ui/enable-status/index.tsx'; -import { SearchLink } from '@/Components/ui/search-link/index.tsx'; -import { PhpInfoConstants } from './constants.ts'; -import { PhpInfoPhpVersion } from './php-version'; -import { PhpInfoStore } from './store.ts'; -export const PhpInfo: FC = memo( - observer(() => { - const { pollData } = PhpInfoStore; - if (!pollData) { - return null; - } - const oneLineItems: [string, ReactNode][] = [ - [ - 'PHP info', - - {gettext('Detail')} - , - ], - [gettext('Version'), ], - ]; - const shortItems: [string, ReactNode][] = [ - [gettext('SAPI interface'), pollData?.sapi], - [ - gettext('Display errors'), - , - ], - [gettext('Error reporting'), pollData.errorReporting], - [gettext('Max memory limit'), pollData.memoryLimit], - [gettext('Max POST size'), pollData.postMaxSize], - [gettext('Max upload size'), pollData.uploadMaxFilesize], - [gettext('Max input variables'), pollData.maxInputVars], - [gettext('Max execution time'), pollData.maxExecutionTime], - [gettext('Timeout for socket'), pollData.defaultSocketTimeout], - [ - gettext('Treatment URLs file'), - , - ], - [ - gettext('SMTP support'), - , - ], - ]; - const { disableFunctions, disableClasses } = pollData; - disableFunctions.slice().sort(); - disableClasses.slice().sort(); - const longItems: [string, ReactNode][] = [ - [ - gettext('Disabled functions'), - disableFunctions.length - ? disableFunctions.map((fn: string) => ( - - )) - : '-', - ], - [ - gettext('Disabled classes'), - disableClasses.length - ? disableClasses.map((fn: string) => ( - - )) - : '-', - ], - ]; - return ( - - - {oneLineItems.map(([title, content]) => ( - - {content} - - ))} - {shortItems.map(([title, content]) => ( - - {content} - - ))} - - - {longItems.map(([title, content]) => ( - - {content} - - ))} - - - ); - }) -); +import { type FC, memo, type ReactNode } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Link } from "@/Components/Button/components/index.tsx"; +import { serverFetchRoute } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { UiMultiColContainer } from "@/Components/ui/col/multi-container.tsx"; +import { UiSingleColContainer } from "@/Components/ui/col/single-container.tsx"; +import { EnableStatus } from "@/Components/ui/enable-status/index.tsx"; +import { SearchLink } from "@/Components/ui/search-link/index.tsx"; +import { PHP_INFO_ID } from "./constants.ts"; +import { PhpInfoPhpVersion } from "./php-version"; +import { usePhpInfoStore } from "./store.ts"; + +export const PhpInfo: FC = memo(() => { + const pollData = usePhpInfoStore(useShallow((s) => s.pollData)); + if (!pollData) { + return null; + } + const oneLineItems: [string, ReactNode][] = [ + [ + "PHP info", + + {gettext("Detail")} + , + ], + [gettext("Version"), ], + ]; + const shortItems: [string, ReactNode][] = [ + [gettext("SAPI interface"), pollData?.sapi], + [ + gettext("Display errors"), + , + ], + [gettext("Error reporting"), pollData.errorReporting], + [gettext("Max memory limit"), pollData.memoryLimit], + [gettext("Max POST size"), pollData.postMaxSize], + [gettext("Max upload size"), pollData.uploadMaxFilesize], + [gettext("Max input variables"), pollData.maxInputVars], + [gettext("Max execution time"), pollData.maxExecutionTime], + [gettext("Timeout for socket"), pollData.defaultSocketTimeout], + [ + gettext("Treatment URLs file"), + , + ], + [ + gettext("SMTP support"), + , + ], + ]; + const { disableFunctions, disableClasses } = pollData; + disableFunctions.sort(); + disableClasses.sort(); + const longItems: [string, ReactNode][] = [ + [ + gettext("Disabled functions"), + disableFunctions.length + ? disableFunctions.map((fn: string) => ( + + )) + : "-", + ], + [ + gettext("Disabled classes"), + disableClasses.length + ? disableClasses.map((fn: string) => ( + + )) + : "-", + ], + ]; + return ( + + + {oneLineItems.map(([title, content]) => ( + + {content} + + ))} + {shortItems.map(([title, content]) => ( + + {content} + + ))} + + + {longItems.map(([title, content]) => ( + + {content} + + ))} + + + ); +}); diff --git a/src/Components/PhpInfo/components/loader.ts b/src/Components/PhpInfo/components/loader.ts index f93e643..7e676eb 100644 --- a/src/Components/PhpInfo/components/loader.ts +++ b/src/Components/PhpInfo/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { PhpInfoConstants } from './constants.ts'; -import { PhpInfo as content } from './index.tsx'; -import { PhpInfoNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { PHP_INFO_ID as id } from "./constants.ts"; +import { PhpInfo as content } from "./index.tsx"; +import { PhpInfoNav as nav } from "./nav.tsx"; + export const PhpInfoLoader: ModuleProps = { - id: PhpInfoConstants.id, content, + id, nav, }; diff --git a/src/Components/PhpInfo/components/nav.tsx b/src/Components/PhpInfo/components/nav.tsx index 01eed00..47a1fbc 100644 --- a/src/Components/PhpInfo/components/nav.tsx +++ b/src/Components/PhpInfo/components/nav.tsx @@ -1,13 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { PhpInfoConstants } from './constants.ts'; -import { PhpInfoStore } from './store.ts'; -export const PhpInfoNav: FC = observer(() => { - const { pollData } = PhpInfoStore; - if (!pollData) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { PHP_INFO_ID } from "./constants.ts"; +import { usePhpInfoStore } from "./store.ts"; + +export const PhpInfoNav: FC = () => { + const hasPollData = usePhpInfoStore((s) => Boolean(s.pollData)); + if (!hasPollData) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/PhpInfo/components/php-version.tsx b/src/Components/PhpInfo/components/php-version.tsx index 9728510..5ad3a1a 100644 --- a/src/Components/PhpInfo/components/php-version.tsx +++ b/src/Components/PhpInfo/components/php-version.tsx @@ -1,18 +1,25 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, useEffect } from 'react'; -import { Link } from '@/Components/Button/components/index.tsx'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { template } from '@/Components/Utils/components/template.ts'; -import { versionCompare } from '@/Components/Utils/components/version-compare.ts'; -import { PhpInfoStore } from './store.ts'; -export const PhpInfoPhpVersion: FC = observer(() => { - const { pollData, latestPhpVersion, setLatestPhpVersion } = PhpInfoStore; +import { type FC, useEffect } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Link } from "@/Components/Button/components/index.tsx"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { template } from "@/Components/Utils/components/template.ts"; +import { versionCompare } from "@/Components/Utils/components/version-compare.ts"; +import { usePhpInfoStore } from "./store.ts"; + +export const PhpInfoPhpVersion: FC = () => { + const { phpVersion, latestPhpVersion, setLatestPhpVersion } = usePhpInfoStore( + useShallow((s) => ({ + latestPhpVersion: s.latestPhpVersion, + phpVersion: s.pollData?.phpVersion ?? "", + setLatestPhpVersion: s.setLatestPhpVersion, + })), + ); useEffect(() => { const fetchData = async () => { const { data, status } = await serverFetch<{ version: string }>( - 'latestPhpVersion' + "latestPhpVersion", ); if (data?.version && status === OK) { setLatestPhpVersion(data.version); @@ -20,22 +27,23 @@ export const PhpInfoPhpVersion: FC = observer(() => { }; fetchData(); }, [setLatestPhpVersion]); - const phpVersion = pollData?.phpVersion ?? ''; const compare = versionCompare(phpVersion, latestPhpVersion); return ( {compare === -1 - ? ` ${template( - gettext('{{oldVersion}} (Latest: {{latestPhpVersion}})'), + ? ` ${ + template( + gettext("{{oldVersion}} (Latest: {{latestPhpVersion}})"), { - oldVersion: phpVersion, latestPhpVersion, - } - )}` + oldVersion: phpVersion, + }, + ) + }` : phpVersion} ); -}); +}; diff --git a/src/Components/PhpInfo/components/store.ts b/src/Components/PhpInfo/components/store.ts index b002ef6..d9b050d 100644 --- a/src/Components/PhpInfo/components/store.ts +++ b/src/Components/PhpInfo/components/store.ts @@ -1,22 +1,23 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { PhpInfoPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { PhpInfoPollDataProps } from "./types.ts"; + +type State = { + pollData: PhpInfoPollDataProps | null; + latestPhpVersion: string; + setPollData: (pollData: PhpInfoPollDataProps | null) => void; + setLatestPhpVersion: (latestPhpVersion: string) => void; +}; +const store: StateCreator = (set) => ({ + latestPhpVersion: "", + pollData: null, + setLatestPhpVersion: (version) => + set((state) => { + state.latestPhpVersion = version; + }), + setPollData: (data) => + set((state) => { + state.pollData = data; + }), }); -class Main { - pollData: PhpInfoPollDataProps | null = null; - latestPhpVersion = ''; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: PhpInfoPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; - setLatestPhpVersion = (latestPhpVersion: string) => { - this.latestPhpVersion = latestPhpVersion; - }; -} -export const PhpInfoStore = new Main(); +export const usePhpInfoStore = create()(immer(store)); diff --git a/src/Components/PhpInfo/components/typings.ts b/src/Components/PhpInfo/components/typings.ts deleted file mode 100644 index 481adac..0000000 --- a/src/Components/PhpInfo/components/typings.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface PhpInfoPollDataProps { - phpVersion: string; - sapi: string; - displayErrors: boolean; - errorReporting: number; - memoryLimit: string; - postMaxSize: string; - uploadMaxFilesize: string; - maxInputVars: number; - maxExecutionTime: number; - defaultSocketTimeout: number; - allowUrlFopen: boolean; - smtp: boolean; - disableFunctions: string[]; - disableClasses: string[]; -} diff --git a/src/Components/Ping/components/constants.ts b/src/Components/Ping/components/constants.ts index db01f24..165a06a 100644 --- a/src/Components/Ping/components/constants.ts +++ b/src/Components/Ping/components/constants.ts @@ -1,3 +1 @@ -export const PingConstants = { - id: 'ping', -}; +export const PING_ID = "ping"; diff --git a/src/Components/Ping/components/index.tsx b/src/Components/Ping/components/index.tsx index cf01399..4fb1228 100644 --- a/src/Components/Ping/components/index.tsx +++ b/src/Components/Ping/components/index.tsx @@ -1,12 +1,11 @@ -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { PingConstants } from './constants.ts'; -import { PingServerToBrowser } from './server-browser.tsx'; -export const Ping: FC = memo(() => { - return ( - - - - ); -}); +import { type FC, memo } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { PING_ID } from "./constants.ts"; +import { PingServerToBrowser } from "./server-browser.tsx"; + +export const Ping: FC = memo(() => ( + + + +)); diff --git a/src/Components/Ping/components/loader.ts b/src/Components/Ping/components/loader.ts index d4213c0..ebcbd7b 100644 --- a/src/Components/Ping/components/loader.ts +++ b/src/Components/Ping/components/loader.ts @@ -1,8 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { PingConstants } from './constants.ts'; -import { Ping as content } from './index.tsx'; -import { PingNav as nav } from './nav.tsx';export const PingLoader: ModuleProps = { - id: PingConstants.id, +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { PING_ID as id } from "./constants.ts"; +import { Ping as content } from "./index.tsx"; +import { PingNav as nav } from "./nav.tsx"; + +export const PingLoader: ModuleProps = { content, + id, nav, }; diff --git a/src/Components/Ping/components/nav.tsx b/src/Components/Ping/components/nav.tsx index 819214e..56bd2b5 100644 --- a/src/Components/Ping/components/nav.tsx +++ b/src/Components/Ping/components/nav.tsx @@ -1,6 +1,8 @@ -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { PingConstants } from './constants.ts';export const PingNav: FC = () => { - return ; -}; +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { PING_ID } from "./constants.ts"; + +export const PingNav: FC = () => ( + +); diff --git a/src/Components/Ping/components/server-browser.tsx b/src/Components/Ping/components/server-browser.tsx index 2fb53fe..0c6b0d2 100644 --- a/src/Components/Ping/components/server-browser.tsx +++ b/src/Components/Ping/components/server-browser.tsx @@ -1,20 +1,22 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, type RefObject, useCallback, useRef } from 'react'; -import { Button } from '@/Components/Button/components/index.tsx'; -import { ButtonStatus } from '@/Components/Button/components/typings.ts'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { calculateMdev } from '@/Components/Utils/components/mdev.ts'; -import { template } from '@/Components/Utils/components/template.ts'; -import { UiSingleColContainer } from '@/Components/ui/col/single-container.tsx'; -import type { ServerToBrowserPingItemProps } from '../typings.ts'; -import { PingStore } from './store.ts'; -import styles from './style.module.scss'; +import { type FC, type RefObject, useCallback, useRef } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Button } from "@/Components/Button/components/index.tsx"; +import { ButtonStatus } from "@/Components/Button/components/types.ts"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { calculateMdev } from "@/Components/Utils/components/mdev.ts"; +import { template } from "@/Components/Utils/components/template.ts"; +import { UiSingleColContainer } from "@/Components/ui/col/single-container.tsx"; +import { usePingStore } from "./store.ts"; +import styles from "./style.module.scss"; +import type { ServerToBrowserPingItemProps } from "./types.ts"; -const Results: FC = observer(() => { - const { serverToBrowserPingItems } = PingStore; +const Results: FC = () => { + const serverToBrowserPingItems = usePingStore( + useShallow((s) => s.serverToBrowserPingItems), + ); const count = serverToBrowserPingItems.length; const items = serverToBrowserPingItems.map(({ time }) => time); const avg = count ? (items.reduce((a, b) => a + b, 0) / count).toFixed(2) : 0; @@ -25,55 +27,64 @@ const Results: FC = observer(() => {
{template( gettext( - '{{times}} times, min/avg/max/mdev = {{min}}/{{avg}}/{{max}}/{{mdev}} ms' + "{{times}} times, min/avg/max/mdev = {{min}}/{{avg}}/{{max}}/{{mdev}} ms", ), - { times: count, min, max, avg, mdev } + { avg, max, mdev, min, times: count }, )}
); -}); +}; const ResultContainer: FC<{ refContainer: RefObject; -}> = observer(({ refContainer }) => { - const { serverToBrowserPingItems } = PingStore; - const count = serverToBrowserPingItems.length; +}> = ({ refContainer }) => { + const serverToBrowserPingItems = usePingStore( + useShallow((s) => s.serverToBrowserPingItems), + ); + const hasServerToBrowserPingItems = Boolean(serverToBrowserPingItems.length); return ( - +
- {!count && '-'} - {Boolean(count) && ( + {!hasServerToBrowserPingItems && "-"} + {hasServerToBrowserPingItems && (
    {serverToBrowserPingItems.map(({ id, time }) => (
  • {`${time} ms`}
  • ))}
)} - {Boolean(count) && } + {hasServerToBrowserPingItems && }
); -}); -export const PingServerToBrowser: FC = observer(() => { +}; +export const PingServerToBrowser: FC = () => { const { - setIsPing, - setIsPingServerToBrowser, - addServerToBrowserPingItem, isPing, isPingServerToBrowser, - } = PingStore; + setIsPingServerToBrowser, + addServerToBrowserPingItem, + } = usePingStore( + useShallow((s) => ({ + addServerToBrowserPingItem: s.addServerToBrowserPingItem, + isPing: s.isPingServerToBrowser || s.isPingServerToServer, + isPingServerToBrowser: s.isPingServerToBrowser, + setIsPingServerToBrowser: s.setIsPingServerToBrowser, + })), + ); const refItemContainer = useRef(null); const refPingTimer = useRef(0); - const SERVER_TIME_MULTIPLIER = 1000; - const TIMEOUT_TIMER_MS = 1000; - const SCROLL_TIMER_MS = 100; + const ServerTimeMultiplier = 1000; + const TimeoutTimerMs = 1000; + const ScrollTimerMs = 100; const ping = useCallback(async (): Promise => { const start = Date.now(); - const { data, status } = - await serverFetch('ping'); + const { data, status } = await serverFetch( + "ping", + ); if (data?.time && status === OK) { const { id, time } = data; const end = Date.now(); - const serverTime = time * SERVER_TIME_MULTIPLIER; + const serverTime = time * ServerTimeMultiplier; addServerToBrowserPingItem({ id, time: Math.floor(end - start - serverTime), @@ -87,44 +98,36 @@ export const PingServerToBrowser: FC = observer(() => { if (st < sh) { refItemContainer.current.scrollTop = sh; } - }, SCROLL_TIMER_MS); + }, ScrollTimerMs); } }, [addServerToBrowserPingItem]); const pingLoop = useCallback(async (): Promise => { await ping(); refPingTimer.current = window.setTimeout(async () => { await pingLoop(); - }, TIMEOUT_TIMER_MS); + }, TimeoutTimerMs); }, [ping]); const handlePing = useCallback(async () => { if (isPing || isPingServerToBrowser) { - setIsPing(false); setIsPingServerToBrowser(false); clearTimeout(refPingTimer.current); return; } - setIsPing(true); setIsPingServerToBrowser(true); await pingLoop(); - }, [ - isPing, - isPingServerToBrowser, - pingLoop, - setIsPing, - setIsPingServerToBrowser, - ]); + }, [isPing, isPingServerToBrowser, pingLoop, setIsPingServerToBrowser]); // const count = serverToBrowserPingItems.length; return ( - + ); -}); +}; diff --git a/src/Components/Ping/components/store.ts b/src/Components/Ping/components/store.ts index 8d16df9..45a57c7 100644 --- a/src/Components/Ping/components/store.ts +++ b/src/Components/Ping/components/store.ts @@ -1,47 +1,51 @@ -import { configure, makeAutoObservable } from 'mobx'; -import type { - ServerToBrowserPingItemProps, - ServerToBrowserPingProps, -} from '../typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { ServerToBrowserPingItemProps } from "./types.ts"; + +type State = { + isPingServerToBrowser: boolean; + isPingServerToServer: boolean; + serverToBrowserPingItems: ServerToBrowserPingItemProps[]; + serverToServerPingItems: ServerToBrowserPingItemProps[]; + // server to server + setIsPingServerToServer: (isPing: boolean) => void; + setServerToServerPingItems: (items: ServerToBrowserPingItemProps[]) => void; + addServerToServerPingItem: (item: ServerToBrowserPingItemProps) => void; + // server to browser + setIsPingServerToBrowser: (isPing: boolean) => void; + setServerToBrowserPingItems: (items: ServerToBrowserPingItemProps[]) => void; + addServerToBrowserPingItem: (item: ServerToBrowserPingItemProps) => void; +}; +const store: StateCreator = (set) => ({ + addServerToBrowserPingItem: (item) => + set((state) => { + state.serverToBrowserPingItems.push(item); + }), + addServerToServerPingItem: (item) => + set((state) => { + state.serverToServerPingItems.push(item); + }), + isPingServerToBrowser: false, + isPingServerToServer: false, + serverToBrowserPingItems: [], + serverToServerPingItems: [], + // server to browser + setIsPingServerToBrowser: (isPing) => + set((state) => { + state.isPingServerToBrowser = isPing; + }), + // server to server + setIsPingServerToServer: (isPing) => + set((state) => { + state.isPingServerToServer = isPing; + }), + setServerToBrowserPingItems: (items) => + set((state) => { + state.serverToBrowserPingItems = items; + }), + setServerToServerPingItems: (items) => + set((state) => { + state.serverToServerPingItems = items; + }), }); -class Main { - isPing = false; - isPingServerToBrowser = false; - isPingServerToServer = false; - serverToBrowserPingItems: ServerToBrowserPingItemProps[] = []; - serverToServerPingItems: ServerToBrowserPingProps[] = []; - constructor() { - makeAutoObservable(this); - } - setIsPing = (isPing: boolean) => { - this.isPing = isPing; - }; - setIsPingServerToBrowser = (isPingServerToBrowser: boolean) => { - this.isPingServerToBrowser = isPingServerToBrowser; - }; - setIsPingServerToServer = (isPingServerToServer: boolean) => { - this.isPingServerToServer = isPingServerToServer; - }; - setServerToBrowserPingItems = ( - serverToBrowserPingItems: ServerToBrowserPingItemProps[] - ) => { - this.serverToBrowserPingItems = serverToBrowserPingItems; - }; - setServerToServerPingItems = ( - serverToServerPingItems: ServerToBrowserPingProps[] - ) => { - this.serverToServerPingItems = serverToServerPingItems; - }; - addServerToBrowserPingItem = ( - serverToBrowserPingItem: ServerToBrowserPingItemProps - ) => { - this.serverToBrowserPingItems.push(serverToBrowserPingItem); - }; - addServerToServerPingItem = ( - serverToServerPingItem: ServerToBrowserPingProps - ) => { - this.serverToServerPingItems.push(serverToServerPingItem); - }; -} -export const PingStore = new Main(); +export const usePingStore = create()(immer(store)); diff --git a/src/Components/Ping/components/style.module.scss b/src/Components/Ping/components/style.module.scss index 1b3c2d1..50786f0 100644 --- a/src/Components/Ping/components/style.module.scss +++ b/src/Components/Ping/components/style.module.scss @@ -1,10 +1,10 @@ :root { --x-ping-result-scrollbar-bg: hsl(0 0% 0% / 0.5); --x-ping-item-bg: hsl(0 0% 0% / 0.1); - @media (prefers-color-scheme: dark) { - --x-ping-result-scrollbar-bg: hsl(0 0% 100% / 0.5); - --x-ping-item-bg: hsl(0 0% 100% / 0.1); - } +} +:global([data-theme="dark"]) { + --x-ping-result-scrollbar-bg: hsl(0 0% 100% / 0.5); + --x-ping-item-bg: hsl(0 0% 100% / 0.1); } .itemContainer { display: grid; diff --git a/src/Components/Ping/typings.ts b/src/Components/Ping/typings.ts deleted file mode 100644 index 140b058..0000000 --- a/src/Components/Ping/typings.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ServerToBrowserPingItemProps { - id: string; - time: number; -} -export interface ServerToBrowserPingProps { - location: string; - items: ServerToBrowserPingItemProps[]; -} diff --git a/src/Components/Placeholder/index.module.scss b/src/Components/Placeholder/index.module.scss index 05dedcb..6b962fb 100644 --- a/src/Components/Placeholder/index.module.scss +++ b/src/Components/Placeholder/index.module.scss @@ -1,15 +1,15 @@ :root { --x-placeholder-bg: linear-gradient(to right, hsl(0 0% 0% / 0.1) 46%, hsl(0 0% 0% / 0.15) 50%, hsl(0 0% 0% / 0.1) 54%) 50% 50%; - @media (prefers-color-scheme: dark) { - --x-placeholder-bg: linear-gradient( - to right, - hsl(0 0% 100% / 0.1) 46%, - hsl(0 0% 100% / 0.15) 50%, - hsl(0 0% 100% / 0.1) 54% - ) - 50% 50%; - } +} +:global([data-theme="dark"]) { + --x-placeholder-bg: linear-gradient( + to right, + hsl(0 0% 100% / 0.1) 46%, + hsl(0 0% 100% / 0.15) 50%, + hsl(0 0% 100% / 0.1) 54% + ) + 50% 50%; } @keyframes animation { 0% { diff --git a/src/Components/Poll/PollAction.php b/src/Components/Poll/PollAction.php index 701d666..7c31394 100644 --- a/src/Components/Poll/PollAction.php +++ b/src/Components/Poll/PollAction.php @@ -30,7 +30,11 @@ final class PollAction extends PoolConstants 'Ping\\PingPoll', ] as $fn) { $class = "\\InnStudio\\Prober\\Components\\{$fn}"; - $data = array_merge($data, (new $class())->render()); + $render = (new $class())->render(); + if ( ! $render || ! $render[array_keys($render)[0]]) { + continue; + } + $data = array_merge($data, $render); } (new RestResponse()) ->setData($data) diff --git a/src/Components/Poll/components/store.ts b/src/Components/Poll/components/store.ts index 159c840..3388d13 100644 --- a/src/Components/Poll/components/store.ts +++ b/src/Components/Poll/components/store.ts @@ -1,16 +1,13 @@ -import { configure, makeAutoObservable } from 'mobx'; -import type { PollDataProps } from './typings.ts'; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { PollData } from "./types.ts"; -configure({ - enforceActions: 'observed', +type State = { + pollData: PollData | null; + setPollData: (pollData: PollData | null) => void; +}; +const actions: StateCreator = (set) => ({ + pollData: null, + setPollData: (pollData) => set(() => ({ pollData })), }); -class Main { - pollData: PollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (data: PollDataProps | null) => { - this.pollData = data; - }; -} -export const PollStore = new Main(); +export const usePollStore = create()(immer(actions)); diff --git a/src/Components/Poll/components/typings.ts b/src/Components/Poll/components/typings.ts deleted file mode 100644 index 6f5a271..0000000 --- a/src/Components/Poll/components/typings.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ConfigProps } from '@/Components/Config/typings.ts'; -import type { DatabasePollDataProps } from '@/Components/Database/components/typings.ts'; -import type { DiskUsagePollDataProps } from '@/Components/DiskUsage/components/typings.ts'; -import type { MyInfoPollDataProps } from '@/Components/MyInfo/components/typings.ts'; -import type { NetworkStatsPollDataProps } from '@/Components/NetworkStats/components/typings.ts'; -import type { NodesPollDataProps } from '@/Components/Nodes/components/typings.ts'; -import type { PhpExtensionsPollDataProps } from '@/Components/PhpExtensions/components/typings.ts'; -import type { PhpInfoPollDataProps } from '@/Components/PhpInfo/components/typings.ts'; -import type { ServerInfoPollDataProps } from '@/Components/ServerInfo/components/typings.ts'; -import type { ServerStatusPollDataProps } from '@/Components/ServerStatus/components/typings.ts'; -import type { TemperatureSensorPollDataProps } from '@/Components/TemperatureSensor/components/typings.ts'; -import type { UserConfigProps } from '@/Components/UserConfig/typings.ts'; - -export interface PollDataProps { - config: ConfigProps | null; - userConfig: UserConfigProps | null; - database: DatabasePollDataProps | null; - myInfo: MyInfoPollDataProps | null; - phpInfo: PhpInfoPollDataProps | null; - diskUsage: DiskUsagePollDataProps | null; - networkStats: NetworkStatsPollDataProps | null; - phpExtensions: PhpExtensionsPollDataProps | null; - serverStatus: ServerStatusPollDataProps | null; - serverInfo: ServerInfoPollDataProps | null; - nodes: NodesPollDataProps | null; - temperatureSensor: TemperatureSensorPollDataProps | null; -} diff --git a/src/Components/ServerBenchmark/components/constants.ts b/src/Components/ServerBenchmark/components/constants.ts index f0fd22e..0b09814 100644 --- a/src/Components/ServerBenchmark/components/constants.ts +++ b/src/Components/ServerBenchmark/components/constants.ts @@ -1,3 +1 @@ -export const ServerBenchmarkConstants = { - id: 'serverBenchmark', -}; +export const SERVER_BENCHMARK_ID = "serverBenchmark"; diff --git a/src/Components/ServerBenchmark/components/index.tsx b/src/Components/ServerBenchmark/components/index.tsx index ab87bbb..087c0ec 100644 --- a/src/Components/ServerBenchmark/components/index.tsx +++ b/src/Components/ServerBenchmark/components/index.tsx @@ -1,26 +1,22 @@ -import { type FC, memo } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { UiDescription } from '@/Components/ui/description/index.tsx'; -import { ServerBenchmarkConstants } from './constants.ts'; -import { ServerBenchmarkServers } from './servers.tsx'; -export const ServerBenchmark: FC = memo(() => { - return ( - - - - - ); -}); +import { type FC, memo } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { UiDescription } from "@/Components/ui/description/index.tsx"; +import { SERVER_BENCHMARK_ID } from "./constants.ts"; +import { ServerBenchmarkServers } from "./servers.tsx"; + +export const ServerBenchmark: FC = memo(() => ( + + + + +)); diff --git a/src/Components/ServerBenchmark/components/loader.ts b/src/Components/ServerBenchmark/components/loader.ts index fd6ef60..5f74f8b 100644 --- a/src/Components/ServerBenchmark/components/loader.ts +++ b/src/Components/ServerBenchmark/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { ServerBenchmarkConstants } from './constants.ts'; -import { ServerBenchmark as content } from './index.tsx'; -import { ServerBenchmarkNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { SERVER_BENCHMARK_ID as id } from "./constants.ts"; +import { ServerBenchmark as content } from "./index.tsx"; +import { ServerBenchmarkNav as nav } from "./nav.tsx"; + export const ServerBenchmarkLoader: ModuleProps = { - id: ServerBenchmarkConstants.id, content, + id, nav, }; diff --git a/src/Components/ServerBenchmark/components/marks-meter.tsx b/src/Components/ServerBenchmark/components/marks-meter.tsx index 254665b..19ed713 100644 --- a/src/Components/ServerBenchmark/components/marks-meter.tsx +++ b/src/Components/ServerBenchmark/components/marks-meter.tsx @@ -1,22 +1,20 @@ -import { MeterCore } from '@/Components/Meter/components/index.tsx'; -import styles from './marks-meter.module.scss'; +import { MeterCore } from "@/Components/Meter/components/index.tsx"; +import styles from "./marks-meter.module.scss"; + export const ServerBenchmarkMarksMeter = ({ totalMarks, total, }: { totalMarks: number; total: number; -}) => { - return ( -
- -
- ); -}; +}) => ( +
+ +
+); diff --git a/src/Components/ServerBenchmark/components/my-server.tsx b/src/Components/ServerBenchmark/components/my-server.tsx index 2f87911..ff986a3 100644 --- a/src/Components/ServerBenchmark/components/my-server.tsx +++ b/src/Components/ServerBenchmark/components/my-server.tsx @@ -1,77 +1,104 @@ -import { observer } from 'mobx-react-lite'; -import { type MouseEvent, useCallback, useState } from 'react'; -import { Button } from '@/Components/Button/components/index.tsx'; -import { ButtonStatus } from '@/Components/Button/components/typings.ts'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { OK, TOO_MANY_REQUESTS } from '@/Components/Rest/http-status.ts'; -import { ToastStore } from '@/Components/Toast/components/store.ts'; -import { template } from '@/Components/Utils/components/template.ts'; -import { ServerBenchmarkItem } from './server-item.tsx'; -import { ServerBenchmarkStore } from './store.ts'; -import type { ServerBenchmarkMarksProps } from './typings.ts'; -export const ServerBenchmarkMyServer = observer(() => { +import { type MouseEvent, useMemo, useState } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Button } from "@/Components/Button/components/index.tsx"; +import { ButtonStatus } from "@/Components/Button/components/types.ts"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { OK, TOO_MANY_REQUESTS } from "@/Components/Rest/http-status.ts"; +import { useToastStore } from "@/Components/Toast/components/store.ts"; +import { template } from "@/Components/Utils/components/template.ts"; +import { ServerBenchmarkItem } from "./server-item.tsx"; +import { useServerBenchmarkStore } from "./store.ts"; +import type { ServerBenchmarkMarksProps } from "./types.ts"; + +export const ServerBenchmarkMyServer = () => { const [benchmarking, setBenchmarking] = useState(false); - const { setMaxMarks, maxMarks } = ServerBenchmarkStore; + const open = useToastStore((s) => s.open); + + const { setMaxMarks, maxMarks } = useServerBenchmarkStore( + useShallow((s) => ({ + maxMarks: s.maxMarks, + setMaxMarks: s.setMaxMarks, + })), + ); + const [marks, setMarks] = useState({ cpu: 0, read: 0, write: 0, }); - const handleBenchmarking = useCallback( - async (e: MouseEvent): Promise => { - e.preventDefault(); - e.stopPropagation(); - if (benchmarking) { - return; - } - // setLinkText(gettext('Testing, please wait...')) - setBenchmarking(true); + + // 1. 优化异步逻辑,移除 benchmarking 依赖,防止函数频繁重建 + const handleBenchmarking = async ( + e: MouseEvent, + ): Promise => { + e.preventDefault(); + e.stopPropagation(); + // 如果已经在测试中,直接拦截 + if (benchmarking) { + return; + } + + setBenchmarking(true); + try { const { data, status } = await serverFetch<{ marks: ServerBenchmarkMarksProps; seconds: number; - }>('benchmarkPerformance'); - setBenchmarking(false); - // const { marks, seconds = 0 } = data || {} - if (status === OK) { - if (data?.marks) { - setMarks(data.marks); - const total = Object.values(data.marks).reduce((a, b) => a + b, 0); - if (total > maxMarks) { - setMaxMarks(total); - } - return; + }>("benchmarkPerformance"); + + if (status === OK && data?.marks) { + setMarks(data.marks); + const total = Object.values(data.marks).reduce((a, b) => a + b, 0); + if (total > maxMarks) { + setMaxMarks(total); } - ToastStore.open(gettext('Network error, please try again later.')); return; } - if (data?.seconds && status === TOO_MANY_REQUESTS) { - ToastStore.open( - template(gettext('Please wait {{seconds}}s'), { + + if (status === TOO_MANY_REQUESTS && data?.seconds) { + open( + template(gettext("Please wait {{seconds}}s"), { seconds: data.seconds, - }) + }), ); return; } - ToastStore.open(gettext('Network error, please try again later.')); - }, - [benchmarking, maxMarks, setMaxMarks] - ); - const date = new Date(); + + open(gettext("Network error, please try again later.")); + } catch (error) { + open(gettext("Network error, please try again later.")); + console.error(error); + } finally { + setBenchmarking(false); + } + }; + + // 2. 规范日期格式,并用 useMemo 锁住引用,避免每次 render 都 new Date() + const formattedDate = useMemo(() => { + const d = new Date(); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const date = String(d.getDate()).padStart(2, "0"); + return `${year}-${month}-${date}`; // 输出标准的 YYYY-MM-DD + }, []); + + // 3. 这里的按钮可以增加 disabled 属性(如果你的 Button 组件支持),在 benchmarking 时原生禁用点击 const header = ( ); + return ( ); -}); +}; diff --git a/src/Components/ServerBenchmark/components/nav.tsx b/src/Components/ServerBenchmark/components/nav.tsx index 188a29b..ae6def2 100644 --- a/src/Components/ServerBenchmark/components/nav.tsx +++ b/src/Components/ServerBenchmark/components/nav.tsx @@ -1,9 +1,8 @@ -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { ServerBenchmarkConstants } from './constants.ts'; -export const ServerBenchmarkNav: FC = () => { - return ( - - ); -}; +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { SERVER_BENCHMARK_ID } from "./constants.ts"; + +export const ServerBenchmarkNav: FC = () => ( + +); diff --git a/src/Components/ServerBenchmark/components/server-item.module.scss b/src/Components/ServerBenchmark/components/server-item.module.scss index f5e4086..46301f1 100644 --- a/src/Components/ServerBenchmark/components/server-item.module.scss +++ b/src/Components/ServerBenchmark/components/server-item.module.scss @@ -2,11 +2,11 @@ --x-server-benchmark-bg: transparent; --x-server-benchmark-link-bg: hsl(0 0% 0% / 0.05); --x-server-benchmark-link-fg: hsl(0 0% 0% / 0.95); - @media (prefers-color-scheme: dark) { - // --x-server-benchmark-bg: hsl(0 0% 100% / 0.05); - --x-server-benchmark-link-fg: hsl(0 0% 100% / 0.95); - --x-server-benchmark-link-bg: hsl(0 0% 100% / 0.05); - } +} +:global([data-theme="dark"]) { + // --x-server-benchmark-bg: hsl(0 0% 100% / 0.05); + --x-server-benchmark-link-fg: hsl(0 0% 100% / 0.95); + --x-server-benchmark-link-bg: hsl(0 0% 100% / 0.05); } .main { display: grid; diff --git a/src/Components/ServerBenchmark/components/server-item.tsx b/src/Components/ServerBenchmark/components/server-item.tsx index 7d3e74e..71aae27 100644 --- a/src/Components/ServerBenchmark/components/server-item.tsx +++ b/src/Components/ServerBenchmark/components/server-item.tsx @@ -1,11 +1,13 @@ -import copyToClipboard from 'copy-to-clipboard'; -import type { FC, MouseEvent, ReactNode } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { template } from '@/Components/Utils/components/template.ts'; -import { UiRuby } from '@/Components/ui/ruby/index.tsx'; -import { ServerBenchmarkMarksMeter } from './marks-meter.tsx'; -import styles from './server-item.module.scss'; -import type { ServerBenchmarkMarksProps } from './typings.ts';const ServerBenchmarkResult: FC<{ +import copyToClipboard from "copy-to-clipboard"; +import type { FC, MouseEvent, ReactNode } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { template } from "@/Components/Utils/components/template.ts"; +import { UiRuby } from "@/Components/ui/ruby/index.tsx"; +import { ServerBenchmarkMarksMeter } from "./marks-meter.tsx"; +import styles from "./server-item.module.scss"; +import type { ServerBenchmarkMarksProps } from "./types.ts"; + +const ServerBenchmarkResult: FC<{ cpu: number; read: number; write: number; @@ -17,7 +19,7 @@ import type { ServerBenchmarkMarksProps } from './typings.ts';const ServerBenchm const writeString = write.toLocaleString(); const totalString = total.toLocaleString(); const totalText = template( - '{{cpu}} (CPU) + {{read}} (Read) + {{write}} (Write) = {{total}}', + "{{cpu}} (CPU) + {{read}} (Read) + {{write}} (Write) = {{total}}", { cpu: cpuString, read: readString, @@ -35,16 +37,16 @@ import type { ServerBenchmarkMarksProps } from './typings.ts';const ServerBenchm ); }; diff --git a/src/Components/ServerBenchmark/components/servers.tsx b/src/Components/ServerBenchmark/components/servers.tsx index 09cc47e..05e54ec 100644 --- a/src/Components/ServerBenchmark/components/servers.tsx +++ b/src/Components/ServerBenchmark/components/servers.tsx @@ -1,120 +1,127 @@ -import { DownloadCloud, Link } from 'lucide-react'; -import { observer } from 'mobx-react-lite'; -import { type FC, useEffect, useState } from 'react'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { Placeholder } from '@/Components/Placeholder/index.tsx'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { UiError } from '@/Components/ui/error/index.tsx'; -import styles from './index.module.scss'; -import { ServerBenchmarkMyServer } from './my-server.tsx'; -import stylesItem from './server-item.module.scss'; -import { ServerBenchmarkItem } from './server-item.tsx'; -import { ServerBenchmarkStore } from './store.ts'; -import type { ServerBenchmarkProps } from './typings.ts'; -export const ServerBenchmarkServers: FC = observer(() => { - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - const { servers, setServers, setMaxMarks, maxMarks } = ServerBenchmarkStore; +import { DownloadCloud, Link } from "lucide-react"; +import { type FC, useEffect, useMemo, useState } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { Placeholder } from "@/Components/Placeholder/index.tsx"; +import { OK } from "@/Components/Rest/http-status.ts"; +import type { FetchStatus } from "@/Components/Utils/components/fetch-status.ts"; +import { UiError } from "@/Components/ui/error/index.tsx"; +import styles from "./index.module.scss"; +import { ServerBenchmarkMyServer } from "./my-server.tsx"; +import stylesItem from "./server-item.module.scss"; +import { ServerBenchmarkItem } from "./server-item.tsx"; +import { useServerBenchmarkStore } from "./store.ts"; +import type { ServerBenchmarkProps } from "./types.ts"; + +export const ServerBenchmarkServers: FC = () => { + const [fetchStatus, setFetchStatus] = useState("loading"); + const { servers, setServers, setMaxMarks, maxMarks } = + useServerBenchmarkStore( + useShallow((s) => ({ + maxMarks: s.maxMarks, + servers: s.servers, + setMaxMarks: s.setMaxMarks, + setServers: s.setServers, + })), + ); useEffect(() => { const fetchData = async () => { - setLoading(true); - const { data, status } = - await serverFetch('benchmarkServers'); - setLoading(false); + setFetchStatus("loading"); + const { data, status } = await serverFetch( + "benchmarkServers", + ); if (!data?.length || status !== OK) { - setError(true); + setFetchStatus("error"); return; } - setError(false); - let marks = 0; - setServers( - data - .map((item) => { - item.total = item.detail - ? Object.values(item.detail).reduce((a, b) => a + b, 0) - : 0; - if (item.total > marks) { - marks = item.total; - } - return item; - }) - .toSorted((a, b) => (b?.total ?? 0) - (a?.total ?? 0)) - ); - setMaxMarks(marks); + const processedServers = data.map((item) => ({ + ...item, + total: item.detail + ? Object.values(item.detail).reduce((a, b) => a + b, 0) + : 0, + })); + processedServers.sort((a, b) => b.total - a.total); + const highestMark = processedServers[0]?.total ?? 0; + setServers(processedServers); + setMaxMarks(highestMark); + setFetchStatus("idel"); }; + fetchData(); }, [setServers, setMaxMarks]); - // const maxMarks = servers.reduce((a, b) => Math.max(a, b?.total ?? 0), 0) - const results = servers.map( - ({ name, url, date, probeUrl, binUrl, detail }) => { - if (!detail) { - return null; - } - const { cpu = 0, read = 0, write = 0 } = detail; - const proberLink = probeUrl ? ( - - - - ) : ( - '' - ); - const binLink = binUrl ? ( - - - - ) : ( - '' - ); - const title = ( - - {name} - - ); - return ( - - {title} - {proberLink} - {binLink} - - } - key={name} - marks={{ cpu, read, write }} - maxMarks={maxMarks} - /> - ); - } - ); + const results = useMemo(() => { + return servers + .filter((server) => server.detail) // 过滤掉没有 detail 的数据,避免产生 null 节点 + .map(({ name, url, date, probeUrl, binUrl, detail }) => { + // 这里的断言是安全的,因为上面 filter 过了 + const { cpu = 0, read = 0, write = 0 } = detail; + + const proberLink = probeUrl + ? ( + + + + ) + : null; + const binLink = binUrl + ? ( + + + + ) + : null; + const title = ( + + {name} + + ); + return ( + + {title} + {proberLink} + {binLink} + + } + key={name} + marks={{ cpu, read, write }} + maxMarks={maxMarks} + /> + ); + }); + }, [servers, maxMarks]); return (
- {loading - ? [...new Array(5)].map(() => ) - : results} - {error && ( - {gettext('Can not fetch marks data from GitHub.')} + {fetchStatus === "loading" && + Array.from({ length: 5 }).map((_, i) => ( + + ))} + {fetchStatus === "idel" && results} + {fetchStatus === "error" && ( + {gettext("Can not fetch marks data from GitHub.")} )}
); -}); +}; diff --git a/src/Components/ServerBenchmark/components/store.ts b/src/Components/ServerBenchmark/components/store.ts index 24abfc8..fe24e0c 100644 --- a/src/Components/ServerBenchmark/components/store.ts +++ b/src/Components/ServerBenchmark/components/store.ts @@ -1,34 +1,42 @@ -import { configure, makeAutoObservable } from 'mobx'; -import type { ServerBenchmarkProps } from './typings.ts'; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { ServerBenchmarkProps } from "./types.ts"; -configure({ - enforceActions: 'observed', -}); -class Main { - benchmarking = false; - maxMarks = 0; - servers: ServerBenchmarkProps[] = []; - constructor() { - makeAutoObservable(this); - } - setMaxMarks = (maxMarks: number) => { - this.maxMarks = maxMarks; - }; - setServers = (servers: ServerBenchmarkProps[]) => { - this.servers = servers; - }; - setServer = ( - id: ServerBenchmarkProps['id'], +type State = { + benchmarking: boolean; + maxMarks: number; + servers: ServerBenchmarkProps[]; + setBenchmarking: (benchmarking: boolean) => void; + setMaxMarks: (maxMarks: number) => void; + setServers: (servers: ServerBenchmarkProps[]) => void; + setServer: ( + id: ServerBenchmarkProps["id"], server: ServerBenchmarkProps - ) => { - const i = this.servers.findIndex((n) => n.id === id); - if (i === -1) { - return; - } - this.servers[i] = server; - }; - setBenchmarking = (benchmarking: boolean) => { - this.benchmarking = benchmarking; - }; -} -export const ServerBenchmarkStore = new Main(); + ) => void; +}; +const store: StateCreator = (set) => ({ + benchmarking: false, + maxMarks: 0, + servers: [], + setBenchmarking: (benchmarking) => + set((state) => { + state.benchmarking = benchmarking; + }), + setMaxMarks: (maxMarks) => + set((state) => { + state.maxMarks = maxMarks; + }), + setServer: (id, server) => + set((state) => { + const i = state.servers.findIndex((n) => n.id === id); + if (i === -1) { + return; + } + state.servers[i] = server; + }), + setServers: (servers) => + set((state) => { + state.servers = servers; + }), +}); +export const useServerBenchmarkStore = create()(immer(store)); diff --git a/src/Components/ServerBenchmark/components/typings.ts b/src/Components/ServerBenchmark/components/typings.ts deleted file mode 100644 index 3c741d4..0000000 --- a/src/Components/ServerBenchmark/components/typings.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface ServerBenchmarkMarksProps { - cpu: number; - read: number; - write: number; -} -export interface ServerBenchmarkProps { - id: string; - name: string; - url: string; - date: string; - probeUrl: string; - binUrl: string; - total: number; - detail: ServerBenchmarkMarksProps; -} diff --git a/src/Components/ServerInfo/components/constants.ts b/src/Components/ServerInfo/components/constants.ts index bb3bdea..0408df9 100644 --- a/src/Components/ServerInfo/components/constants.ts +++ b/src/Components/ServerInfo/components/constants.ts @@ -1,3 +1 @@ -export const ServerInfoConstants = { - id: 'serverInfo', -}; +export const SERVER_INFO_ID = "serverInfo"; diff --git a/src/Components/ServerInfo/components/index.tsx b/src/Components/ServerInfo/components/index.tsx index c5f6fd2..e7b6d67 100644 --- a/src/Components/ServerInfo/components/index.tsx +++ b/src/Components/ServerInfo/components/index.tsx @@ -1,30 +1,31 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, memo, type ReactNode, useEffect } from 'react'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { Location } from '@/Components/Location/components/index.tsx'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { OK } from '@/Components/Rest/http-status.ts'; -import { template } from '@/Components/Utils/components/template'; -import { UiMultiColContainer } from '@/Components/ui/col/multi-container.tsx'; -import { UiSingleColContainer } from '@/Components/ui/col/single-container.tsx'; -import { ServerInfoConstants } from './constants.ts'; -import { ServerInfoStore } from './store.ts'; -import type { ServerInfoPollDataProps } from './typings.ts'; +import { type FC, memo, type ReactNode, useEffect } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { gettext } from "@/Components/Language/index.ts"; +import { Location } from "@/Components/Location/components/index.tsx"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { OK } from "@/Components/Rest/http-status.ts"; +import { template } from "@/Components/Utils/components/template"; +import { UiMultiColContainer } from "@/Components/ui/col/multi-container.tsx"; +import { UiSingleColContainer } from "@/Components/ui/col/single-container.tsx"; +import { SERVER_INFO_ID } from "./constants.ts"; +import { useServerInfoStore } from "./store.ts"; +import type { ServerInfoPollDataProps } from "./types.ts"; +const DEFAULT_UPTIME = { days: 0, hours: 0, mins: 0, secs: 0 }; const ServerTime: FC<{ - serverUptime: ServerInfoPollDataProps['serverUptime']; - serverTime: ServerInfoPollDataProps['serverTime']; -}> = observer(({ serverUptime, serverTime }) => { + serverUptime: ServerInfoPollDataProps["serverUptime"]; + serverTime: ServerInfoPollDataProps["serverTime"]; +}> = ({ serverUptime, serverTime }) => { const { days, hours, mins, secs } = serverUptime; const uptime = template( - gettext('{{days}}d {{hours}}h {{mins}}min {{secs}}s'), - { days, hours, mins, secs } + gettext("{{days}}d {{hours}}h {{mins}}min {{secs}}s"), + { days, hours, mins, secs }, ); const items = [ - [gettext('Time'), serverTime], - [gettext('Uptime'), uptime], + [gettext("Time"), serverTime], + [gettext("Uptime"), uptime], ]; return ( <> @@ -35,21 +36,21 @@ const ServerTime: FC<{ ))} ); -}); +}; const SingleItems: FC<{ - cpuModel: ServerInfoPollDataProps['cpuModel']; - serverOs: ServerInfoPollDataProps['serverOs']; - scriptPath: ServerInfoPollDataProps['scriptPath']; + cpuModel: ServerInfoPollDataProps["cpuModel"]; + serverOs: ServerInfoPollDataProps["serverOs"]; + scriptPath: ServerInfoPollDataProps["scriptPath"]; publicIpv4: string; }> = memo(({ cpuModel, serverOs, scriptPath, publicIpv4 }) => { const items: [string, ReactNode][] = [ [ - gettext('Location (IPv4)'), + gettext("Location (IPv4)"), , ], - [gettext('CPU model'), cpuModel ?? gettext('Unavailable')], - [gettext('OS'), serverOs ?? gettext('Unavailable')], - [gettext('Script path'), scriptPath ?? gettext('Unavailable')], + [gettext("CPU model"), cpuModel ?? gettext("Unavailable")], + [gettext("OS"), serverOs ?? gettext("Unavailable")], + [gettext("Script path"), scriptPath ?? gettext("Unavailable")], ]; return ( @@ -62,8 +63,8 @@ const SingleItems: FC<{ ); }); const MultiItems: FC<{ - serverName: ServerInfoPollDataProps['serverName']; - serverSoftware: ServerInfoPollDataProps['serverSoftware']; + serverName: ServerInfoPollDataProps["serverName"]; + serverSoftware: ServerInfoPollDataProps["serverSoftware"]; publicIpv4: string; publicIpv6: string; localIpv4: string; @@ -78,12 +79,12 @@ const MultiItems: FC<{ localIpv6, }) => { const items: [string, ReactNode][] = [ - [gettext('Name'), serverName ?? gettext('Unavailable')], - [gettext('Web server'), serverSoftware ?? gettext('Unavailable')], - [gettext('Public IPv4'), publicIpv4 || '-'], - [gettext('Public IPv6'), publicIpv6 || '-'], - [gettext('Local IPv4'), localIpv4 || '-'], - [gettext('Local IPv6'), localIpv6 || '-'], + [gettext("Name"), serverName ?? gettext("Unavailable")], + [gettext("Web server"), serverSoftware ?? gettext("Unavailable")], + [gettext("Public IPv4"), publicIpv4 || "-"], + [gettext("Public IPv6"), publicIpv6 || "-"], + [gettext("Local IPv4"), localIpv4 || "-"], + [gettext("Local IPv6"), localIpv6 || "-"], ]; return ( <> @@ -94,16 +95,50 @@ const MultiItems: FC<{ ))} ); - } + }, ); -export const ServerInfo: FC = observer(() => { - const { pollData, publicIpv4, publicIpv6, setPublicIpv4, setPublicIpv6 } = - ServerInfoStore; +export const LiveUptime: FC = () => { + const { serverTime, serverUptime } = useServerInfoStore( + useShallow((s) => ({ + serverTime: s.pollData?.serverTime ?? "-", + serverUptime: s.pollData?.serverUptime ?? DEFAULT_UPTIME, + })), + ); + return ; +}; +export const ServerInfo: FC = () => { + const setPublicIpv4 = useServerInfoStore((s) => s.setPublicIpv4); + const setPublicIpv6 = useServerInfoStore((s) => s.setPublicIpv6); + const { + hasPollData, + publicIpv4, + publicIpv6, + localIpv4, + localIpv6, + serverName, + serverSoftware, + cpuModel, + scriptPath, + serverOs, + } = useServerInfoStore( + useShallow((s) => ({ + cpuModel: s.pollData?.cpuModel ?? "-", + hasPollData: Boolean(s.pollData), + localIpv4: s.pollData?.localIpv4 ?? "-", + localIpv6: s.pollData?.localIpv6 ?? "-", + publicIpv4: s.publicIpv4, + publicIpv6: s.publicIpv6, + scriptPath: s.pollData?.scriptPath ?? "-", + serverName: s.pollData?.serverName ?? "-", + serverOs: s.pollData?.serverOs ?? "-", + serverSoftware: s.pollData?.serverSoftware ?? "-", + })), + ); // fetch ipv4 useEffect(() => { const fetchData = async () => { const { data, status } = await serverFetch<{ ip: string }>( - 'serverPublicIpv4' + "serverPublicIpv4", ); if (data?.ip && status === OK) { setPublicIpv4(data.ip); @@ -115,7 +150,7 @@ export const ServerInfo: FC = observer(() => { useEffect(() => { const fetchData = async () => { const { data, status } = await serverFetch<{ ip: string }>( - 'serverPublicIpv6' + "serverPublicIpv6", ); if (data?.ip && status === OK) { setPublicIpv6(data.ip); @@ -123,31 +158,28 @@ export const ServerInfo: FC = observer(() => { }; fetchData(); }, [setPublicIpv6]); - if (!pollData) { + if (!hasPollData) { return null; } return ( - + - + ); -}); +}; diff --git a/src/Components/ServerInfo/components/loader.ts b/src/Components/ServerInfo/components/loader.ts index 4526584..fb07546 100644 --- a/src/Components/ServerInfo/components/loader.ts +++ b/src/Components/ServerInfo/components/loader.ts @@ -1,8 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { ServerInfoConstants } from './constants.ts'; -import { ServerInfo as content } from './index.tsx'; -import { ServerInfoNav as nav } from './nav.tsx';export const ServerInfoLoader: ModuleProps = { - id: ServerInfoConstants.id, +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { SERVER_INFO_ID as id } from "./constants.ts"; +import { ServerInfo as content } from "./index.tsx"; +import { ServerInfoNav as nav } from "./nav.tsx"; + +export const ServerInfoLoader: ModuleProps = { content, + id, nav, }; diff --git a/src/Components/ServerInfo/components/nav.tsx b/src/Components/ServerInfo/components/nav.tsx index 0df73c2..de2a34d 100644 --- a/src/Components/ServerInfo/components/nav.tsx +++ b/src/Components/ServerInfo/components/nav.tsx @@ -1,12 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { ServerInfoConstants } from './constants.ts'; -import { ServerInfoStore } from './store.ts';export const ServerInfoNav: FC = observer(() => { - const { pollData } = ServerInfoStore; - if (!pollData) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { SERVER_INFO_ID } from "./constants.ts"; +import { useServerInfoStore } from "./store.ts"; + +export const ServerInfoNav: FC = () => { + const hasPollData = useServerInfoStore((s) => Boolean(s.pollData)); + if (!hasPollData) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/ServerInfo/components/store.ts b/src/Components/ServerInfo/components/store.ts index 30fc51e..226adc0 100644 --- a/src/Components/ServerInfo/components/store.ts +++ b/src/Components/ServerInfo/components/store.ts @@ -1,26 +1,30 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { ServerInfoPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { ServerInfoPollDataProps } from "./types.ts"; + +type State = { + pollData: ServerInfoPollDataProps | null; + publicIpv4: string; + publicIpv6: string; + setPollData: (pollData: ServerInfoPollDataProps | null) => void; + setPublicIpv4: (ipv4: string) => void; + setPublicIpv6: (ipv6: string) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + publicIpv4: "", + publicIpv6: "", + setPollData: (pollData) => + set((state) => { + state.pollData = pollData; + }), + setPublicIpv4: (publicIpv4) => + set((state) => { + state.publicIpv4 = publicIpv4; + }), + setPublicIpv6: (publicIpv6) => + set((state) => { + state.publicIpv6 = publicIpv6; + }), }); -class Main { - pollData: ServerInfoPollDataProps | null = null; - publicIpv4 = ''; - publicIpv6 = ''; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: ServerInfoPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; - setPublicIpv4 = (ipv4: string) => { - this.publicIpv4 = ipv4; - }; - setPublicIpv6 = (ipv6: string) => { - this.publicIpv6 = ipv6; - }; -} -export const ServerInfoStore = new Main(); +export const useServerInfoStore = create()(immer(store)); diff --git a/src/Components/ServerInfo/components/typings.ts b/src/Components/ServerInfo/components/typings.ts deleted file mode 100644 index 54ddda7..0000000 --- a/src/Components/ServerInfo/components/typings.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface ServerInfoUptimeProps { - days: number; - hours: number; - mins: number; - secs: number; -} -export interface ServerInfoPollDataProps { - serverName: string; - serverUtcTime: string; - serverTime: string; - localIpv4: string; - localIpv6: string; - serverUptime: ServerInfoUptimeProps; - serverIp: string; - serverSoftware: string; - phpVersion: string; - cpuModel: string; - serverOs: string; - scriptPath: string; -} diff --git a/src/Components/ServerStatus/components/constants.ts b/src/Components/ServerStatus/components/constants.ts index 86b78af..550caf1 100644 --- a/src/Components/ServerStatus/components/constants.ts +++ b/src/Components/ServerStatus/components/constants.ts @@ -1,3 +1 @@ -export const ServerStatusConstants = { - id: 'serverStatus', -}; +export const SERVER_STATUS_ID = "serverStatus"; diff --git a/src/Components/ServerStatus/components/cpu-usage.tsx b/src/Components/ServerStatus/components/cpu-usage.tsx index 8d1097d..5e68aa3 100644 --- a/src/Components/ServerStatus/components/cpu-usage.tsx +++ b/src/Components/ServerStatus/components/cpu-usage.tsx @@ -1,28 +1,34 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { template } from '@/Components/Utils/components/template'; -import { ServerStatusStore } from './store.ts'; -export const CpuUsage: FC = observer(() => { - const { cpuUsage } = ServerStatusStore; - const { idle } = cpuUsage; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { template } from "@/Components/Utils/components/template"; +import { useServerStatusStore } from "./store.ts"; + +export const CpuUsage: FC = () => { + const { idle, sys, user } = useServerStatusStore( + useShallow((s) => ({ + idle: s.pollData.cpuUsage.idle, + sys: s.pollData.cpuUsage.sys, + user: s.pollData.cpuUsage.user, + })), + ); return ( ); -}); +}; diff --git a/src/Components/ServerStatus/components/index.tsx b/src/Components/ServerStatus/components/index.tsx index 52441a1..e05f859 100644 --- a/src/Components/ServerStatus/components/index.tsx +++ b/src/Components/ServerStatus/components/index.tsx @@ -1,16 +1,17 @@ -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { ServerStatusConstants } from './constants.ts'; -import styles from './index.module.scss'; -import { MemBuffers } from './mem-buffers'; -import { MemCached } from './mem-cached'; -import { MemRealUsage } from './mem-real-usage'; -import { SwapCached } from './swap-cached'; -import { SwapUsage } from './swap-usage'; -import { SystemLoad } from './system-load'; +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { SERVER_STATUS_ID } from "./constants.ts"; +import styles from "./index.module.scss"; +import { MemBuffers } from "./mem-buffers"; +import { MemCached } from "./mem-cached"; +import { MemRealUsage } from "./mem-real-usage"; +import { SwapCached } from "./swap-cached"; +import { SwapUsage } from "./swap-usage"; +import { SystemLoad } from "./system-load"; + export const ServerStatus: FC = () => ( - +
diff --git a/src/Components/ServerStatus/components/loader.ts b/src/Components/ServerStatus/components/loader.ts index 366325c..d7e46b1 100644 --- a/src/Components/ServerStatus/components/loader.ts +++ b/src/Components/ServerStatus/components/loader.ts @@ -1,9 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { ServerStatusConstants } from './constants.ts'; -import { ServerStatus as content } from './index.tsx'; -import { ServerStatusNav as nav } from './nav.tsx'; +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { SERVER_STATUS_ID as id } from "./constants.ts"; +import { ServerStatus as content } from "./index.tsx"; +import { ServerStatusNav as nav } from "./nav.tsx"; + export const ServerStatusLoader: ModuleProps = { - id: ServerStatusConstants.id, content, + id, nav, }; diff --git a/src/Components/ServerStatus/components/mem-buffers.tsx b/src/Components/ServerStatus/components/mem-buffers.tsx index 22470e5..ce58cae 100644 --- a/src/Components/ServerStatus/components/mem-buffers.tsx +++ b/src/Components/ServerStatus/components/mem-buffers.tsx @@ -1,19 +1,25 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { ServerStatusStore } from './store.ts'; -export const MemBuffers: FC = observer(() => { - const { max, value } = ServerStatusStore.memBuffers; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { useServerStatusStore } from "./store.ts"; + +export const MemBuffers: FC = () => { + const { max, value } = useServerStatusStore( + useShallow((s) => ({ + max: s.pollData.memBuffers.max, + value: s.pollData.memBuffers.value, + })), + ); return ( ); -}); +}; diff --git a/src/Components/ServerStatus/components/mem-cached.tsx b/src/Components/ServerStatus/components/mem-cached.tsx index 3799af3..8da2387 100644 --- a/src/Components/ServerStatus/components/mem-cached.tsx +++ b/src/Components/ServerStatus/components/mem-cached.tsx @@ -1,19 +1,25 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { ServerStatusStore } from './store.ts'; -export const MemCached: FC = observer(() => { - const { max, value } = ServerStatusStore.memCached; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { useServerStatusStore } from "./store.ts"; + +export const MemCached: FC = () => { + const { max, value } = useServerStatusStore( + useShallow((s) => ({ + max: s.pollData.memCached.max, + value: s.pollData.memCached.value, + })), + ); return ( ); -}); +}; diff --git a/src/Components/ServerStatus/components/mem-real-usage.tsx b/src/Components/ServerStatus/components/mem-real-usage.tsx index 34099d6..96712f0 100644 --- a/src/Components/ServerStatus/components/mem-real-usage.tsx +++ b/src/Components/ServerStatus/components/mem-real-usage.tsx @@ -1,19 +1,25 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { ServerStatusStore } from './store.ts'; -export const MemRealUsage: FC = observer(() => { - const { max, value } = ServerStatusStore.memRealUsage; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { useServerStatusStore } from "./store.ts"; + +export const MemRealUsage: FC = () => { + const { max, value } = useServerStatusStore( + useShallow((s) => ({ + max: s.pollData.memRealUsage.max, + value: s.pollData.memRealUsage.value, + })), + ); return ( ); -}); +}; diff --git a/src/Components/ServerStatus/components/nav.tsx b/src/Components/ServerStatus/components/nav.tsx index 7a133ac..0f9186c 100644 --- a/src/Components/ServerStatus/components/nav.tsx +++ b/src/Components/ServerStatus/components/nav.tsx @@ -1,12 +1,13 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { ServerStatusConstants } from './constants.ts'; -import { ServerStatusStore } from './store.ts';export const ServerStatusNav: FC = observer(() => { - const { pollData } = ServerStatusStore; - if (!pollData) { +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { SERVER_STATUS_ID } from "./constants.ts"; +import { useServerStatusStore } from "./store.ts"; + +export const ServerStatusNav: FC = () => { + const hasPollData = useServerStatusStore((s) => Boolean(s.pollData)); + if (!hasPollData) { return null; } - return ; -}); + return ; +}; diff --git a/src/Components/ServerStatus/components/store.ts b/src/Components/ServerStatus/components/store.ts index dd4c1b2..d85a30e 100644 --- a/src/Components/ServerStatus/components/store.ts +++ b/src/Components/ServerStatus/components/store.ts @@ -1,71 +1,58 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { ServerStatusPollDataProps } from './typings';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { ServerStatusPollDataProps } from "./types"; + +type State = { + pollData: ServerStatusPollDataProps | null; + setPollData: (pollData: ServerStatusPollDataProps | null) => void; +}; +const initPollData: ServerStatusPollDataProps = { + cpuUsage: { idle: 100, sys: 0, usage: 0, user: 0 }, + memBuffers: { max: 0, value: 0 }, + memCached: { max: 0, value: 0 }, + memRealUsage: { max: 0, value: 0 }, + swapCached: { max: 0, value: 0 }, + swapUsage: { max: 0, value: 0 }, + sysLoad: [0, 0, 0], +}; +const store: StateCreator = (set) => ({ + pollData: initPollData, + setPollData: (pollData) => + set((state) => { + state.pollData = { ...state.pollData, ...pollData }; + }), + // sysLoad: () => get().pollData?.sysLoad || [0, 0, 0], + // cpuUsage: () => + // get().pollData?.cpuUsage ?? { + // usage: 0, + // idle: 100, + // sys: 0, + // user: 0, + // }, + // memRealUsage: () => + // get().pollData?.memRealUsage ?? { + // max: 0, + // value: 0, + // }, + // memCached: () => + // get().pollData?.memCached ?? { + // max: 0, + // value: 0, + // }, + // memBuffers: () => + // get().pollData?.memBuffers ?? { + // max: 0, + // value: 0, + // }, + // swapUsage: () => + // get().pollData?.swapUsage ?? { + // max: 0, + // value: 0, + // }, + // swapCached: () => + // get().pollData?.swapCached ?? { + // max: 0, + // value: 0, + // }, }); -class Main { - pollData: ServerStatusPollDataProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: ServerStatusPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; - get sysLoad(): ServerStatusPollDataProps['sysLoad'] { - return this.pollData?.sysLoad || [0, 0, 0]; - } - get cpuUsage(): ServerStatusPollDataProps['cpuUsage'] { - return ( - this.pollData?.cpuUsage ?? { - usage: 0, - idle: 100, - sys: 0, - user: 0, - } - ); - } - get memRealUsage(): ServerStatusPollDataProps['memRealUsage'] { - return ( - this.pollData?.memRealUsage ?? { - max: 0, - value: 0, - } - ); - } - get memCached(): ServerStatusPollDataProps['memCached'] { - return ( - this.pollData?.memCached ?? { - max: 0, - value: 0, - } - ); - } - get memBuffers(): ServerStatusPollDataProps['memBuffers'] { - return ( - this.pollData?.memBuffers ?? { - max: 0, - value: 0, - } - ); - } - get swapUsage(): ServerStatusPollDataProps['swapUsage'] { - return ( - this.pollData?.swapUsage ?? { - max: 0, - value: 0, - } - ); - } - get swapCached(): ServerStatusPollDataProps['swapCached'] { - return ( - this.pollData?.swapCached ?? { - max: 0, - value: 0, - } - ); - } -} -export const ServerStatusStore = new Main(); +export const useServerStatusStore = create()(immer(store)); diff --git a/src/Components/ServerStatus/components/swap-cached.tsx b/src/Components/ServerStatus/components/swap-cached.tsx index 5b2e3b9..45ad67a 100644 --- a/src/Components/ServerStatus/components/swap-cached.tsx +++ b/src/Components/ServerStatus/components/swap-cached.tsx @@ -1,14 +1,20 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { ServerStatusStore } from './store.ts'; -export const SwapCached: FC = observer(() => { - const { max, value } = ServerStatusStore.swapCached; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { useServerStatusStore } from "./store.ts"; + +export const SwapCached: FC = () => { + const { max, value } = useServerStatusStore( + useShallow((s) => ({ + max: s.pollData.swapCached.max, + value: s.pollData.swapCached.value, + })), + ); if (!max) { return null; } return ( - + ); -}); +}; diff --git a/src/Components/ServerStatus/components/swap-usage.tsx b/src/Components/ServerStatus/components/swap-usage.tsx index a89c475..858eb11 100644 --- a/src/Components/ServerStatus/components/swap-usage.tsx +++ b/src/Components/ServerStatus/components/swap-usage.tsx @@ -1,14 +1,20 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components/index.tsx'; -import { ServerStatusStore } from './store.ts'; -export const SwapUsage: FC = observer(() => { - const { max, value } = ServerStatusStore.swapUsage; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components/index.tsx"; +import { useServerStatusStore } from "./store.ts"; + +export const SwapUsage: FC = () => { + const { max, value } = useServerStatusStore( + useShallow((s) => ({ + max: s.pollData.swapUsage.max, + value: s.pollData.swapUsage.value, + })), + ); if (!max) { return null; } return ( - + ); -}); +}; diff --git a/src/Components/ServerStatus/components/system-load.module.scss b/src/Components/ServerStatus/components/system-load.module.scss index eef92ba..c5c7728 100644 --- a/src/Components/ServerStatus/components/system-load.module.scss +++ b/src/Components/ServerStatus/components/system-load.module.scss @@ -2,11 +2,11 @@ --x-sys-load-fg: var(--x-fg); --x-sys-load-bg: transparent; --x-sys-load-interval-bg: hsl(0 0% 0% / 0.1); - @media (prefers-color-scheme: dark) { - --x-sys-load-fg: var(--x-fg); - // --x-sys-load-bg: hsl(0 0% 100% / 0.05); - --x-sys-load-interval-bg: hsl(0 0% 100% / 0.1); - } +} +:global([data-theme="dark"]) { + --x-sys-load-fg: var(--x-fg); + // --x-sys-load-bg: hsl(0 0% 100% / 0.05); + --x-sys-load-interval-bg: hsl(0 0% 100% / 0.1); } .main { display: grid; diff --git a/src/Components/ServerStatus/components/system-load.tsx b/src/Components/ServerStatus/components/system-load.tsx index 0bcf8e2..e3c10ab 100644 --- a/src/Components/ServerStatus/components/system-load.tsx +++ b/src/Components/ServerStatus/components/system-load.tsx @@ -1,20 +1,19 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { MeterCore } from '@/Components/Meter/components/index.tsx'; -import { template } from '@/Components/Utils/components/template'; -import { ServerStatusStore } from './store.ts'; -import styles from './system-load.module.scss'; +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { MeterCore } from "@/Components/Meter/components/index.tsx"; +import { template } from "@/Components/Utils/components/template"; +import { useServerStatusStore } from "./store.ts"; +import styles from "./system-load.module.scss"; + export const SysLoadItem: FC<{ load: number; title?: string }> = ({ load, title, -}) => { - return ( -
- {load.toFixed(2)} -
- ); -}; +}) => ( +
+ {load.toFixed(2)} +
+); export const SysLoadGroup: FC<{ sysLoad: number[]; }> = ({ sysLoad }) => { @@ -22,7 +21,7 @@ export const SysLoadGroup: FC<{ const loadHuman = sysLoad.map((load, i) => ({ id: `${minutes[i]}minAvg`, load, - text: template(gettext('{{minute}} minute average'), { + text: template(gettext("{{minute}} minute average"), { minute: minutes[i], }), })); @@ -36,8 +35,13 @@ export const SysLoadGroup: FC<{
); }; -export const SystemLoad: FC = observer(() => { - const { sysLoad, cpuUsage } = ServerStatusStore; +export const SystemLoad: FC = () => { + const { sysLoad, cpuUsage } = useServerStatusStore( + useShallow((s) => ({ + cpuUsage: s.pollData.cpuUsage, + sysLoad: s.pollData.sysLoad, + })), + ); const cpuTotal = cpuUsage.user + cpuUsage.idle + cpuUsage.sys; const cpuTitle = ` user: ${((cpuUsage.user / cpuTotal) * 100).toFixed(2)}% @@ -46,14 +50,14 @@ sys: ${((cpuUsage.sys / cpuTotal) * 100).toFixed(2)}% `; return (
-
{gettext('System load')}
+
{gettext("System load")}
- {template(gettext('{{usage}}% CPU usage'), { usage: cpuUsage.usage })} + {template(gettext("{{usage}}% CPU usage"), { usage: cpuUsage.usage })}
100 ? 100 : cpuUsage.usage} />
); -}); +}; diff --git a/src/Components/ServerStatus/components/typings.ts b/src/Components/ServerStatus/components/typings.ts deleted file mode 100644 index bde25c2..0000000 --- a/src/Components/ServerStatus/components/typings.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface ServerStatusUsageProps { - max: number; - value: number; -} -export interface ServerStatusCpuUsageProps { - usage: number; - idle: number; - sys: number; - user: number; -} -export interface ServerStatusPollDataProps { - sysLoad: number[]; - cpuUsage: ServerStatusCpuUsageProps; - memRealUsage: ServerStatusUsageProps; - memBuffers: ServerStatusUsageProps; - memCached: ServerStatusUsageProps; - swapUsage: ServerStatusUsageProps; - swapCached: ServerStatusUsageProps; -} diff --git a/src/Components/TemperatureSensor/TemperatureSensorPoll.php b/src/Components/TemperatureSensor/TemperatureSensorPoll.php index ee13b51..d5e18e1 100644 --- a/src/Components/TemperatureSensor/TemperatureSensorPoll.php +++ b/src/Components/TemperatureSensor/TemperatureSensorPoll.php @@ -3,7 +3,6 @@ namespace InnStudio\Prober\Components\TemperatureSensor; use Exception; -use InnStudio\Prober\Components\Config\ConfigApi; use InnStudio\Prober\Components\UserConfig\UserConfigApi; final class TemperatureSensorPoll @@ -17,30 +16,17 @@ final class TemperatureSensorPoll ]; } $items = $this->getItems(); - if ( ! $items) { - return [ - $id => null, - ]; - } - if ($items) { - return [ - $id => $items, - ]; - } $cpuTemp = $this->getCpuTemp(); - if ( ! $cpuTemp) { - return [ - $id => null, + if (false !== $cpuTemp) { + $items[] = [ + 'id' => 'cpu', + 'name' => 'CPU', + 'celsius' => round($cpuTemp / 1000, 2), ]; } - $items[] = [ - 'id' => 'cpu', - 'name' => 'CPU', - 'celsius' => round((float) $cpuTemp / 1000, 2), - ]; return [ - $id => $items, + $id => $items ?: null, ]; } @@ -53,9 +39,9 @@ final class TemperatureSensorPoll curl_setopt_array($ch, [ \CURLOPT_URL => $url, \CURLOPT_RETURNTRANSFER => true, + \CURLOPT_TIMEOUT => 2, ]); $res = curl_exec($ch); - curl_close($ch); return (string) $res; } @@ -63,9 +49,12 @@ final class TemperatureSensorPoll private function getItems() { $items = []; - foreach (ConfigApi::$config['APP_TEMPERATURE_SENSOR_PORTS'] as $port) { - // check curl - $res = $this->curl(ConfigApi::$config['APP_TEMPERATURE_SENSOR_URL'] . ":{$port}"); + $urls = UserConfigApi::get('temperatureSensors') ?: []; + if ( ! $urls) { + return []; + } + foreach ($urls as $url) { + $res = $this->curl($url); if ( ! $res) { continue; } @@ -73,8 +62,7 @@ final class TemperatureSensorPoll if ( ! $item || ! \is_array($item)) { continue; } - $items = $item; - break; + $items[] = $item; } return $items; @@ -84,10 +72,13 @@ final class TemperatureSensorPoll { try { $path = '/sys/class/thermal/thermal_zone0/temp'; + if ( ! is_readable($path)) { + return false; + } - return file_exists($path) ? (int) file_get_contents($path) : 0; + return (float) file_get_contents($path); } catch (Exception $e) { - return 0; + return false; } } } diff --git a/src/Components/TemperatureSensor/components/constants.ts b/src/Components/TemperatureSensor/components/constants.ts index 752320d..b3fe864 100644 --- a/src/Components/TemperatureSensor/components/constants.ts +++ b/src/Components/TemperatureSensor/components/constants.ts @@ -1,3 +1,4 @@ export const TemperatureSensorConstants = { - id: 'temperatureSensor', + id: "temperatureSensor", }; +export const TEMPERATURE_SENSOR_ID = "temperatureSensor"; diff --git a/src/Components/TemperatureSensor/components/index.tsx b/src/Components/TemperatureSensor/components/index.tsx index b51131a..c7a9bf6 100644 --- a/src/Components/TemperatureSensor/components/index.tsx +++ b/src/Components/TemperatureSensor/components/index.tsx @@ -1,29 +1,31 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Meter } from '@/Components/Meter/components'; -import { ModuleGroup } from '@/Components/Module/components/group.tsx'; -import { ModuleItem } from '@/Components/Module/components/item.tsx'; -import { template } from '@/Components/Utils/components/template'; -import { UiSingleColContainer } from '@/Components/ui/col/single-container.tsx'; -import { TemperatureSensorConstants } from './constants.ts'; -import { TemperatureSensorStore } from './store.ts'; -export const TemperatureSensor: FC = observer(() => { - const { pollData } = TemperatureSensorStore; - if (!pollData?.items?.length) { +import type { FC } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { gettext } from "@/Components/Language/index.ts"; +import { Meter } from "@/Components/Meter/components"; +import { ModuleGroup } from "@/Components/Module/components/group.tsx"; +import { ModuleItem } from "@/Components/Module/components/item.tsx"; +import { template } from "@/Components/Utils/components/template"; +import { UiSingleColContainer } from "@/Components/ui/col/single-container.tsx"; +import { TEMPERATURE_SENSOR_ID } from "./constants.ts"; +import { useTemperatureSensorStore } from "./store.ts"; + +export const TemperatureSensor: FC = () => { + const items = useTemperatureSensorStore( + useShallow((s) => s.pollData?.items ?? []), + ); + if (!items.length) { return null; } - const { items } = pollData; return ( {items.map(({ id, name, celsius }) => ( @@ -38,4 +40,4 @@ export const TemperatureSensor: FC = observer(() => { ); -}); +}; diff --git a/src/Components/TemperatureSensor/components/loader.ts b/src/Components/TemperatureSensor/components/loader.ts index b2ea913..eb2b964 100644 --- a/src/Components/TemperatureSensor/components/loader.ts +++ b/src/Components/TemperatureSensor/components/loader.ts @@ -1,8 +1,10 @@ -import type { ModuleProps } from '@/Components/Module/components/typings.ts'; -import { TemperatureSensorConstants } from './constants.ts'; -import { TemperatureSensor as content } from './index.tsx'; -import { TemperatureSensorNav as nav } from './nav.tsx';export const TemperatureSensorLoader: ModuleProps = { - id: TemperatureSensorConstants.id, +import type { ModuleProps } from "@/Components/Module/components/types.ts"; +import { TEMPERATURE_SENSOR_ID as id } from "./constants.ts"; +import { TemperatureSensor as content } from "./index.tsx"; +import { TemperatureSensorNav as nav } from "./nav.tsx"; + +export const TemperatureSensorLoader: ModuleProps = { content, + id, nav, }; diff --git a/src/Components/TemperatureSensor/components/nav.tsx b/src/Components/TemperatureSensor/components/nav.tsx index 7b5a09b..3fdf6e6 100644 --- a/src/Components/TemperatureSensor/components/nav.tsx +++ b/src/Components/TemperatureSensor/components/nav.tsx @@ -1,21 +1,15 @@ -import { observer } from 'mobx-react-lite'; -import type { FC } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { NavItem } from '@/Components/Nav/components/item.tsx'; -import { TemperatureSensorConstants } from './constants.ts'; -import { TemperatureSensorStore } from './store.ts';export const TemperatureSensorNav: FC = observer(() => { - const { pollData } = TemperatureSensorStore; - if (!pollData?.items?.length) { - return null; - } - const { items } = pollData; - if (!items.length) { - return null; - } - return ( - +import type { FC } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { NavItem } from "@/Components/Nav/components/item.tsx"; +import { TEMPERATURE_SENSOR_ID } from "./constants.ts"; +import { useTemperatureSensorStore } from "./store.ts"; + +export const TemperatureSensorNav: FC = () => { + const hasPollData = useTemperatureSensorStore((s) => + Boolean(s.pollData?.items.length) ); -}); + if (!hasPollData) { + return null; + } + return ; +}; diff --git a/src/Components/TemperatureSensor/components/store.ts b/src/Components/TemperatureSensor/components/store.ts index b010f64..4988518 100644 --- a/src/Components/TemperatureSensor/components/store.ts +++ b/src/Components/TemperatureSensor/components/store.ts @@ -1,22 +1,27 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '@/Components/Utils/components/is-deep-equal/index.ts'; -import type { TemperatureSensorPollDataProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { isDeepEqual } from "@/Components/Utils/components/is-deep-equal/index.ts"; +import type { TemperatureSensorPollDataProps } from "./types.ts"; + +type State = { + pollData: TemperatureSensorPollDataProps | null; + latestPhpVersion: string; + setPollData: (pollData: TemperatureSensorPollDataProps | null) => void; + setLatestPhpVersion: (latestPhpVersion: string) => void; +}; +const store: StateCreator = (set) => ({ + latestPhpVersion: "", + pollData: null, + setLatestPhpVersion: (latestPhpVersion) => + set((state) => { + state.latestPhpVersion = latestPhpVersion; + }), + setPollData: (pollData) => + set((state) => { + if (isDeepEqual(pollData, state.pollData)) { + return; + } + state.pollData = pollData; + }), }); -class Main { - pollData: TemperatureSensorPollDataProps | null = null; - latestPhpVersion = ''; - constructor() { - makeAutoObservable(this); - } - setPollData = (pollData: TemperatureSensorPollDataProps | null) => { - if (isDeepEqual(pollData, this.pollData)) { - return; - } - this.pollData = pollData; - }; - setLatestPhpVersion = (latestPhpVersion: string) => { - this.latestPhpVersion = latestPhpVersion; - }; -} -export const TemperatureSensorStore = new Main(); +export const useTemperatureSensorStore = create()(immer(store)); diff --git a/src/Components/TemperatureSensor/components/typings.ts b/src/Components/TemperatureSensor/components/typings.ts deleted file mode 100644 index a4a39af..0000000 --- a/src/Components/TemperatureSensor/components/typings.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface TemperatureSensorItemProps { - id: string; - name: string; - celsius: number; -} -export interface TemperatureSensorPollDataProps { - items: TemperatureSensorItemProps[]; -} diff --git a/src/Components/Toast/components/index.module.scss b/src/Components/Toast/components/index.module.scss index 74cdbdb..58f0fd7 100644 --- a/src/Components/Toast/components/index.module.scss +++ b/src/Components/Toast/components/index.module.scss @@ -1,7 +1,7 @@ :root { --x-toast-fg: hsl(0 0% 100% / 0.95); --x-toast-bg: hsl(0 0% 0% / 0.75); - @media (prefers-color-scheme: dark) { + :global([data-theme="dark"]) { --x-toast-fg: hsl(0 0% 100% / 0.95); --x-toast-bg: hsl(0 0% 100% / 0.15); } diff --git a/src/Components/Toast/components/index.tsx b/src/Components/Toast/components/index.tsx index cb90066..b5c08a8 100644 --- a/src/Components/Toast/components/index.tsx +++ b/src/Components/Toast/components/index.tsx @@ -1,16 +1,20 @@ -import { observer } from 'mobx-react-lite'; -import type { FC, MouseEvent } from 'react'; -import { gettext } from '@/Components/Language/index.ts'; -import { Portal } from '@/Components/Utils/components/portal.tsx'; -import styles from './index.module.scss'; -import { ToastStore } from './store.ts'; -export const Toast: FC = observer(() => { - const { isOpen, msg, close } = ToastStore; - const handleClose = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - close(); - }; +import { type FC, type MouseEvent, useCallback } from "react"; +import { gettext } from "@/Components/Language/index.ts"; +import { Portal } from "@/Components/Utils/components/portal.tsx"; +import styles from "./index.module.scss"; +import { useToastStore } from "./store.ts"; +export const Toast: FC = () => { + const isOpen = useToastStore((s) => s.isOpen); + const msg = useToastStore((s) => s.msg); + const close = useToastStore((s) => s.close); + const handleClose = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + close(); + }, + [close] + ); if (!isOpen) { return null; } @@ -19,11 +23,11 @@ export const Toast: FC = observer(() => { ); -}); +}; diff --git a/src/Components/Toast/components/store.ts b/src/Components/Toast/components/store.ts index 461f61d..fecdf87 100644 --- a/src/Components/Toast/components/store.ts +++ b/src/Components/Toast/components/store.ts @@ -1,26 +1,55 @@ -import { configure, makeAutoObservable, runInAction } from 'mobx'; -import type { ReactNode } from 'react';configure({ - enforceActions: 'observed', -}); -class Main { - isOpen = false; - msg: ReactNode = ''; - constructor() { - makeAutoObservable(this); - } - setMsg = (msg: ReactNode) => { - this.msg = msg; - }; - close = (dalaySeconds = 0) => { - setTimeout(() => { - runInAction(() => { - this.isOpen = false; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +type State = { + isOpen: boolean; + msg: string; + timerId: number | null; + setMsg: (msg: string) => void; + open: (msg?: string) => void; + close: (delaySeconds?: number) => void; +}; +const createStore: StateCreator = ( + set, + get +) => ({ + close: (delaySeconds = 0) => { + const currentTimerId = get().timerId; + if (currentTimerId) { + clearTimeout(currentTimerId); + } + if (delaySeconds === 0) { + set((state) => { + state.isOpen = false; + state.timerId = null; }); - }, dalaySeconds * 1000); - }; - open = (msg?: ReactNode) => { - this.msg = msg; - this.isOpen = true; - }; -} -export const ToastStore = new Main(); + return; + } + const id = setTimeout(() => { + set((state) => { + state.isOpen = false; + state.timerId = null; + }); + }, delaySeconds * 1000); + set((state) => { + state.timerId = id; + }); + }, + isOpen: false, + msg: "", + open: (msg) => + set((state) => { + if (state.timerId) { + clearTimeout(state.timerId); + } + state.isOpen = true; + state.msg = msg ?? ""; + state.timerId = null; + }), + setMsg: (msg) => + set((state) => { + state.msg = msg; + }), + timerId: null, +}); +export const useToastStore = create()(immer(createStore)); diff --git a/src/Components/Updater/components/store.ts b/src/Components/Updater/components/store.ts index c7c82e7..7622479 100644 --- a/src/Components/Updater/components/store.ts +++ b/src/Components/Updater/components/store.ts @@ -1,44 +1,32 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { ConfigStore } from '@/Components/Config/store.ts'; -import { gettext } from '@/Components/Language/index.ts'; -import { template } from '@/Components/Utils/components/template'; +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; -configure({ - enforceActions: 'observed', +type State = { + isUpdating: boolean; + hasUpdateError: boolean; + targetVersion: string; + setTargetVersion: (targetVersion: string) => void; + setIsUpdating: (isUpdating: boolean) => void; + setHasUpdateError: (hasUpdateError: boolean) => void; +}; +const createStore: StateCreator = (set) => ({ + isUpdating: false, + hasUpdateError: false, + targetVersion: "", + setTargetVersion: (targetVersion: string) => { + set((state) => { + state.targetVersion = targetVersion; + }); + }, + setIsUpdating: (isUpdating: boolean) => { + set((state) => { + state.isUpdating = isUpdating; + }); + }, + setHasUpdateError: (hasUpdateError: boolean) => { + set((state) => { + state.hasUpdateError = hasUpdateError; + }); + }, }); -class Main { - isUpdating = false; - isUpdateError = false; - targetVersion = ''; - constructor() { - makeAutoObservable(this); - } - setTargetVersion = (targetVersion: string) => { - this.targetVersion = targetVersion; - }; - setIsUpdating = (isUpdating: boolean) => { - this.isUpdating = isUpdating; - }; - setIsUpdateError = (isUpdateError: boolean) => { - this.isUpdateError = isUpdateError; - }; - get notiText(): string { - if (this.isUpdating) { - return gettext('⏳ Updating, please wait a second...'); - } - if (this.isUpdateError) { - return gettext('❌ Update error, click here to try again?'); - } - if (this.targetVersion) { - return template( - gettext('✨ Found new version: {{oldVersion}} ⇢ {{newVersion}}'), - { - oldVersion: ConfigStore.pollData?.APP_VERSION ?? '-', - newVersion: this.targetVersion, - } - ); - } - return ''; - } -} -export const UpdaterStore = new Main(); +export const useUpdaterStore = create()(immer(createStore)); diff --git a/src/Components/Updater/components/updater-link.tsx b/src/Components/Updater/components/updater-link.tsx index b23f56a..4828b8b 100644 --- a/src/Components/Updater/components/updater-link.tsx +++ b/src/Components/Updater/components/updater-link.tsx @@ -1,20 +1,22 @@ -import { observer } from 'mobx-react-lite'; -import { type FC, type MouseEvent, useCallback } from 'react'; -import { serverFetch } from '@/Components/Fetch/server-fetch.ts'; -import { HeaderButton } from '@/Components/Header/components/link.tsx'; -import { gettext } from '@/Components/Language/index.ts'; +import { type FC, type MouseEvent, useCallback } from "react"; +import { serverFetch } from "@/Components/Fetch/server-fetch.ts"; +import { HeaderButton } from "@/Components/Header/components/link.tsx"; +import { gettext } from "@/Components/Language/index.ts"; import { CREATED, FORBIDDEN, INSUFFICIENT_STORAGE, INTERNAL_SERVER_ERROR, -} from '@/Components/Rest/http-status.ts'; -import { ToastStore } from '@/Components/Toast/components/store.ts'; -import { UpdaterStore } from './store.ts'; -export const UpdaterLink: FC = observer(() => { - const { isUpdating, setIsUpdating, setIsUpdateError, notiText } = - UpdaterStore; - const { open } = ToastStore; +} from "@/Components/Rest/http-status.ts"; +import { useToastStore } from "@/Components/Toast/components/store.ts"; +import { useUpdaterStore } from "./store.ts"; +import { useUpdateNotiText } from "./use-update-noti-text.ts"; +export const UpdaterLink: FC = () => { + const notiText = useUpdateNotiText(); + const isUpdating = useUpdaterStore((s) => s.isUpdating); + const setIsUpdating = useUpdaterStore((s) => s.setIsUpdating); + const setHasUpdateError = useUpdaterStore((s) => s.setHasUpdateError); + const open = useToastStore((s) => s.open); const handleUpdate = useCallback( async (e: MouseEvent) => { e.preventDefault(); @@ -23,38 +25,38 @@ export const UpdaterLink: FC = observer(() => { return; } setIsUpdating(true); - const { status } = await serverFetch('update'); + const { status } = await serverFetch("update"); switch (status) { case CREATED: - open(gettext('Update success, refreshing...')); + open(gettext("Update success, refreshing...")); window.location.reload(); return; case FORBIDDEN: - open(gettext('Update is disabled in dev mode.')); + open(gettext("Update is disabled in dev mode.")); setIsUpdating(false); - setIsUpdateError(true); + setHasUpdateError(true); return; case INSUFFICIENT_STORAGE: case INTERNAL_SERVER_ERROR: open( gettext( - 'Can not update file, please check the server permissions and space.' + "Can not update file, please check the server permissions and space." ) ); setIsUpdating(false); - setIsUpdateError(true); + setHasUpdateError(true); return; default: } - open(gettext('Network error, please try again later.')); + open(gettext("Network error, please try again later.")); setIsUpdating(false); - setIsUpdateError(true); + setHasUpdateError(true); }, - [isUpdating, setIsUpdating, setIsUpdateError, open] + [isUpdating, setIsUpdating, setHasUpdateError, open] ); return ( - + {notiText} ); -}); +}; diff --git a/src/Components/UserConfig/UserConfigApi.php b/src/Components/UserConfig/UserConfigApi.php index e79fb60..59a037e 100644 --- a/src/Components/UserConfig/UserConfigApi.php +++ b/src/Components/UserConfig/UserConfigApi.php @@ -2,13 +2,13 @@ namespace InnStudio\Prober\Components\UserConfig; -use InnStudio\Prober\Components\Utils\UtilsApi; +use InnStudio\Prober\Components\Utils\UtilsTomlParser; final class UserConfigApi { private static $conf; - private static $filename = 'xconfig.json'; + private static $filename = 'xconfig.toml'; public static function isDisabled($id) { @@ -30,11 +30,21 @@ final class UserConfigApi if ( ! \defined('XPROBER_DIR')) { return ''; } + $filename = self::$filename; if (\defined('XPROBER_IS_DEV') && XPROBER_IS_DEV) { - return \dirname(XPROBER_DIR) . '/' . self::$filename; + $path = \dirname(XPROBER_DIR) . "/{$filename}"; + if ( ! file_exists($path) || ! is_readable($path)) { + return ''; + } + + return $path; + } + $path = XPROBER_DIR . "/{$filename}"; + if ( ! file_exists($path) || ! is_readable($path)) { + return ''; } - return XPROBER_DIR . '/' . self::$filename; + return $path; } private static function setConf() @@ -42,12 +52,20 @@ final class UserConfigApi if (null !== self::$conf) { return; } - if ( ! is_readable(self::getFilePath())) { + $path = self::getFilePath(); + if ( ! $path) { self::$conf = null; return; } - $conf = UtilsApi::jsonDecode(file_get_contents(self::getFilePath())); + $content = file_get_contents($path); + if ( ! $content) { + self::$conf = null; + + return; + } + // toml + $conf = UtilsTomlParser::parse($content); if ( ! $conf) { self::$conf = null; diff --git a/src/Components/UserConfig/store.ts b/src/Components/UserConfig/store.ts index 6685709..472d92a 100644 --- a/src/Components/UserConfig/store.ts +++ b/src/Components/UserConfig/store.ts @@ -1,18 +1,16 @@ -import { configure, makeAutoObservable } from 'mobx'; -import { isDeepEqual } from '../Utils/components/is-deep-equal/index.ts'; -import type { UserConfigProps } from './typings.ts';configure({ - enforceActions: 'observed', +import { create, type StateCreator } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { UserConfigProps } from "./types.ts"; + +type State = { + pollData: UserConfigProps | null; + setPollData: (pollData: UserConfigProps | null) => void; +}; +const store: StateCreator = (set) => ({ + pollData: null, + setPollData: (data) => + set((state) => { + state.pollData = data; + }), }); -class Main { - data: UserConfigProps | null = null; - constructor() { - makeAutoObservable(this); - } - setPollData = (data: UserConfigProps | null) => { - if (isDeepEqual(data, this.data)) { - return; - } - this.data = data; - }; -} -export const UserConfigStore = new Main(); +export const useUserConfigStore = create()(immer(store)); diff --git a/src/Components/UserConfig/typings.ts b/src/Components/UserConfig/typings.ts deleted file mode 100644 index e6e1b53..0000000 --- a/src/Components/UserConfig/typings.ts +++ /dev/null @@ -1,55 +0,0 @@ -export type UserConfigDisableFeatureKey = - | 'ServerStatus' - | 'DiskUsage' - | 'NetworkStats' - | 'Ping' - | 'ServerInfo' - | 'PhpInfo' - | 'PhpInfoDetail' - | 'PhpDisabledFunctions' - | 'PhpDisabledClasses' - | 'PhpExtensions' - | 'PhpExtensionsLoaded' - | 'Database' - | 'MyServerBenchmark' - | 'MyInfo' - | 'ServerIp'; -export type UserConfigDisableFeatureValue = - | 'serverStatus' - | 'diskUsage' - | 'networkStats' - | 'ping' - | 'serverInfo' - | 'phpInfo' - | 'phpInfoDetail' - | 'phpDisabledFunctions' - | 'phpDisabledClasses' - | 'phpExtensions' - | 'phpExtensionsLoaded' - | 'database' - | 'myServerBenchmark' - | 'myInfo' - | 'serverIp'; -export const UserConfigDisableFeature = { - ServerStatus: 'serverStatus', - DiskUsage: 'diskUsage', - NetworkStats: 'networkStats', - Ping: 'ping', - ServerInfo: 'serverInfo', - PhpInfo: 'phpInfo', - PhpInfoDetail: 'phpInfoDetail', - PhpDisabledFunctions: 'phpDisabledFunctions', - PhpDisabledClasses: 'phpDisabledClasses', - PhpExtensions: 'phpExtensions', - PhpExtensionsLoaded: 'phpExtensionsLoaded', - Database: 'database', - MyServerBenchmark: 'myServerBenchmark', - MyInfo: 'myInfo', - ServerIp: 'serverIp', -}; -export type UserConfigNodeProps = [nodeName: string, url: string]; -export interface UserConfigProps { - serverBenchmarkCd?: number; - nodes?: UserConfigNodeProps[]; - disabled?: UserConfigDisableFeatureKey; -} diff --git a/src/Components/WindowConfig/components/index.ts b/src/Components/WindowConfig/components/index.ts index a470a55..33bf7c8 100644 --- a/src/Components/WindowConfig/components/index.ts +++ b/src/Components/WindowConfig/components/index.ts @@ -1,9 +1,9 @@ -import type { WindowConfigProps, WindowProps } from './typings.ts'; +import type { WindowConfigProps, WindowProps } from "./types.ts"; export const WindowConfig = { + AUTHORIZATION: String( + (window as unknown as WindowProps)?.GLOBAL_CONFIG?.AUTHORIZATION ?? "" + ), IS_DEV: Boolean( (window as unknown as WindowProps)?.GLOBAL_CONFIG?.IS_DEV ?? false ), - AUTHORIZATION: String( - (window as unknown as WindowProps)?.GLOBAL_CONFIG?.AUTHORIZATION ?? '' - ), } as const satisfies WindowConfigProps; diff --git a/src/Components/WindowConfig/components/typings.ts b/src/Components/WindowConfig/components/typings.ts deleted file mode 100644 index ff47a10..0000000 --- a/src/Components/WindowConfig/components/typings.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface WindowConfigProps { - IS_DEV: boolean; - AUTHORIZATION: string; -} -export interface WindowProps { - GLOBAL_CONFIG: WindowConfig; -} diff --git a/src/Components/ui/col/single-container.tsx b/src/Components/ui/col/single-container.tsx index 4c0222c..734b36e 100644 --- a/src/Components/ui/col/single-container.tsx +++ b/src/Components/ui/col/single-container.tsx @@ -1,5 +1,6 @@ -import type { FC, HTMLProps } from 'react'; -import styles from './single.module.scss'; +import type { FC, HTMLProps } from "react"; +import styles from "./single.module.scss"; + export const UiSingleColContainer: FC> = (props) => (
); diff --git a/src/Components/ui/description/index.module.scss b/src/Components/ui/description/index.module.scss index 14af549..c44351d 100644 --- a/src/Components/ui/description/index.module.scss +++ b/src/Components/ui/description/index.module.scss @@ -2,11 +2,11 @@ --x-card-des-fg: var(--x-fg); --x-card-des-bg: hsl(0 0% 100% / 0.1); --x-card-des-accent: hsl(0 0% 0% / 0.5); - @media (prefers-color-scheme: dark) { - --x-card-des-fg: var(--x-fg); - --x-card-des-bg: hsl(0 0% 100% / 0.1); - --x-card-des-accent: hsl(209, 100%, 63%); - } +} +:global([data-theme="dark"]) { + --x-card-des-fg: var(--x-fg); + --x-card-des-bg: hsl(0 0% 100% / 0.1); + --x-card-des-accent: hsl(209, 100%, 63%); } .main { display: grid; diff --git a/src/Components/ui/enable-status/index.tsx b/src/Components/ui/enable-status/index.tsx index f031d4f..c1e623b 100644 --- a/src/Components/ui/enable-status/index.tsx +++ b/src/Components/ui/enable-status/index.tsx @@ -1,9 +1,10 @@ -import type { FC, ReactNode } from 'react'; -import styles from './index.module.scss'; +import type { FC, ReactNode } from "react"; +import styles from "./index.module.scss"; + export const EnableStatus: FC<{ isEnable: boolean; text?: ReactNode; -}> = ({ isEnable, text = '' }) => ( +}> = ({ isEnable, text = "" }) => (
> = ({ children }) => (
{children} diff --git a/src/Components/ui/pie-chart/index.module.scss b/src/Components/ui/pie-chart/index.module.scss index f362e1f..34a69c8 100644 --- a/src/Components/ui/pie-chart/index.module.scss +++ b/src/Components/ui/pie-chart/index.module.scss @@ -4,13 +4,13 @@ --x-pip-chat-fg-medium: hsl(49, 87%, 41%); --x-pip-chat-fg-high: hsl(0, 74%, 49%); --x-pip-chat-text-fg: hsl(0 0% 0%); - @media (prefers-color-scheme: dark) { - --x-pip-chat-bg: hsl(0 0% 100% / 0.1); - --x-pip-chat-fg-low: hsl(106, 87%, 41%); - --x-pip-chat-fg-medium: hsl(49, 87%, 41%); - --x-pip-chat-fg-high: hsl(0, 74%, 49%); - --x-pip-chat-text-fg: hsl(0 0% 100%); - } +} +:global([data-theme="dark"]) { + --x-pip-chat-bg: hsl(0 0% 100% / 0.1); + --x-pip-chat-fg-low: hsl(106, 87%, 41%); + --x-pip-chat-fg-medium: hsl(49, 87%, 41%); + --x-pip-chat-fg-high: hsl(0, 74%, 49%); + --x-pip-chat-text-fg: hsl(0 0% 100%); } .pieBg { stroke: var(--x-pip-chat-bg); diff --git a/src/Components/ui/pie-chart/index.tsx b/src/Components/ui/pie-chart/index.tsx index e2a3bd8..ec08639 100644 --- a/src/Components/ui/pie-chart/index.tsx +++ b/src/Components/ui/pie-chart/index.tsx @@ -1,6 +1,7 @@ -import type { FC } from 'react'; -import styles from './index.module.scss'; -import type { PieChartStatusKey } from './typings.ts'; +import type { FC } from "react"; +import styles from "./index.module.scss"; +import type { PieChartStatusKey } from "./types.ts"; + export const PieChart: FC<{ percent: number; status: PieChartStatusKey; diff --git a/src/Components/ui/pie-chart/typings.ts b/src/Components/ui/pie-chart/typings.ts deleted file mode 100644 index ab80eb0..0000000 --- a/src/Components/ui/pie-chart/typings.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type PieChartStatusKey = 'Low' | 'Medium' | 'High'; -export type PieChartStatusValue = 'low' | 'medium' | 'high'; -export const PieChartStatus = { - Low: 'low', - Medium: 'medium', - High: 'high', -} satisfies Record; diff --git a/src/Components/ui/ruby/index.module.scss b/src/Components/ui/ruby/index.module.scss index 4a03560..50d27cc 100644 --- a/src/Components/ui/ruby/index.module.scss +++ b/src/Components/ui/ruby/index.module.scss @@ -1,20 +1,4 @@ -:root { - --x-benchmark-ruby-bg: hsl(0 0% 0% / 0.05); - --x-benchmark-ruby-bg-hover: hsl(0 0% 0% / 0.05); - @media (prefers-color-scheme: dark) { - --x-benchmark-ruby-bg: hsl(0 0% 100% / 0.05); - --x-benchmark-ruby-bg-hover: hsl(0 0% 100% / 0.1); - } -} .main { - // cursor: pointer; - // border-radius: var(--x-radius); - // background: var(--x-benchmark-ruby-bg); - // padding: var(--x-gutter-sm) var(--x-gutter); - // &:hover { - // background: var(--x-benchmark-ruby-bg-hover); - // text-decoration: none; - // } rt { opacity: 0.5; font-weight: normal; diff --git a/src/Components/ui/search-link/index.module.scss b/src/Components/ui/search-link/index.module.scss index b44b664..4cfab3d 100644 --- a/src/Components/ui/search-link/index.module.scss +++ b/src/Components/ui/search-link/index.module.scss @@ -3,12 +3,12 @@ --x-search-bg: hsl(0 0% 0% / 0.1); --x-search-bg-hover: hsl(0 0% 0% / 0.15); --x-search-bg-active: hsl(0 0% 0% / 0.2); - @media (prefers-color-scheme: dark) { - --x-search-fg: var(--x-fg); - --x-search-bg: hsl(0 0% 100% / 0.1); - --x-search-bg-hover: hsl(0 0% 100% / 0.15); - --x-search-bg-active: hsl(0 0% 100% / 0.2); - } +} +:global([data-theme="dark"]) { + --x-search-fg: var(--x-fg); + --x-search-bg: hsl(0 0% 100% / 0.1); + --x-search-bg-hover: hsl(0 0% 100% / 0.15); + --x-search-bg-active: hsl(0 0% 100% / 0.2); } .main { border-radius: var(--x-radius); diff --git a/xconfig.json b/xconfig.json deleted file mode 100644 index c6dba47..0000000 --- a/xconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - // You can disable listed features: serverStatus, diskUsage, networkStats, ping, serverInfo, phpInfo, phpInfoDetail, phpDisabledFunctions, phpDisabledClasses, phpExtensions, phpExtensionsLoaded, database, myServerBenchmark, myInfo, serverIp - // "disabled": ["serverIp", "phpInfoDetail"], - - // The server benchmark cooldown (seconds) - "serverBenchmarkCd": 30, - - // You can set nodes for X-Prober - "nodes": [ - // ["node-1", "http://localhost:5173"], - // ["node-3", "http://localhost:5173"] - ] -}