diff --git a/CLAUDE.md b/CLAUDE.md index 4229d82..d4b3f8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,6 +25,19 @@ - `pnpm build` でビルドすること +## 型チェックについて + +- `tsconfig.json` の `skipLibCheck` を `true` にしないこと +- `pnpm typecheck` で型チェックを実行し、問題がある場合は修正すること + +## フォーマッターについて + +- `pnpm fmt` でフォーマッターを実行すること + +## リンターについて + +- `pnpm lint` でリンターを実行し、問題がある場合は修正すること + ## コミットについて - 勝手にコミットしないこと diff --git a/biome.jsonc b/biome.jsonc index 7767e79..98c92ac 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -38,6 +38,17 @@ "lineWidth": 100 } }, + "css": { + "parser": { + "cssModules": false + }, + "linter": { + "enabled": false + }, + "formatter": { + "enabled": false + } + }, "javascript": { "formatter": { "enabled": true, diff --git a/index.html b/index.html index 57d1dc4..b2e5b3f 100644 --- a/index.html +++ b/index.html @@ -54,26 +54,8 @@ -
-

DuckDB-Wasm (OPFS) + Parquet + S3-compatible object storage

-

GitHub: https://github.com/voluntas/duckdb-wasm-parquet

-

-

-

OPFS: false

-

Counted: 0

-
- - - - - -
- -
-
- - +
+ \ No newline at end of file diff --git a/package.json b/package.json index 2792143..f83434f 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,17 @@ "version": "0.0.0", "license": "Apache-2.0", "private": true, + "type": "module", "scripts": { "dev": "vite", + "preview": "vite preview", "test:vitest": "vitest run", "test:playwright": "playwright test", "build": "vite build", "lint": "biome lint src/", - "fmt": "pnpm biome format --write src/ tests/", - "check": "tsc --noEmit" + "fmt": "biome format --write src/", + "fmt:check": "biome format src/", + "typecheck": "tsc --noEmit" }, "engines": { "node": ">=24", @@ -26,14 +29,26 @@ "@duckdb/duckdb-wasm": "1.29.1-dev269.0", "@replit/codemirror-vim": "6.3.0", "apache-arrow": "21.0.0", - "codemirror": "6.0.2" + "codemirror": "6.0.2", + "react": "19.1.1", + "react-dom": "19.1.1", + "uplot": "1.6.32", + "wouter": "3.7.1", + "zustand": "5.0.7" }, "devDependencies": { "@biomejs/biome": "2.2.0", "@playwright/test": "1.54.2", + "@tailwindcss/vite": "4.1.12", + "@types/emscripten": "^1.40.1", + "@types/react": "19.1.10", + "@types/react-dom": "19.1.7", + "@vitejs/plugin-react": "5.0.0", + "fast-check": "4.2.0", "playwright": "1.54.2", + "tailwindcss": "4.1.12", "typescript": "5.9.2", "vite": "7.1.2", "vitest": "3.2.4" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71e3919..73805e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,21 @@ importers: codemirror: specifier: 6.0.2 version: 6.0.2 + react: + specifier: 19.1.1 + version: 19.1.1 + react-dom: + specifier: 19.1.1 + version: 19.1.1(react@19.1.1) + uplot: + specifier: 1.6.32 + version: 1.6.32 + wouter: + specifier: 3.7.1 + version: 3.7.1(react@19.1.1) + zustand: + specifier: 5.0.7 + version: 5.0.7(@types/react@19.1.10)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) devDependencies: '@biomejs/biome': specifier: 2.2.0 @@ -45,21 +60,129 @@ importers: '@playwright/test': specifier: 1.54.2 version: 1.54.2 + '@tailwindcss/vite': + specifier: 4.1.12 + version: 4.1.12(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)) + '@types/emscripten': + specifier: ^1.40.1 + version: 1.40.1 + '@types/react': + specifier: 19.1.10 + version: 19.1.10 + '@types/react-dom': + specifier: 19.1.7 + version: 19.1.7(@types/react@19.1.10) + '@vitejs/plugin-react': + specifier: 5.0.0 + version: 5.0.0(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)) + fast-check: + specifier: 4.2.0 + version: 4.2.0 playwright: specifier: 1.54.2 version: 1.54.2 + tailwindcss: + specifier: 4.1.12 + version: 4.1.12 typescript: specifier: 5.9.2 version: 5.9.2 vite: specifier: 7.1.2 - version: 7.1.2(@types/node@24.3.0)(lightningcss@1.30.1) + version: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@24.3.0)(lightningcss@1.30.1)(msw@2.4.9(typescript@5.9.2)) + version: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(msw@2.4.9(typescript@5.9.2)) packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@2.2.0': resolution: {integrity: sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==} engines: {node: '>=14.21.3'} @@ -325,9 +448,26 @@ packages: resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} engines: {node: '>=18'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@lezer/common@1.2.3': resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} @@ -367,6 +507,9 @@ packages: '@codemirror/state': 6.x.x '@codemirror/view': 6.x.x + '@rolldown/pluginutils@1.0.0-beta.30': + resolution: {integrity: sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==} + '@rollup/rollup-android-arm-eabi@4.46.2': resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} cpu: [arm] @@ -470,6 +613,108 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tailwindcss/node@4.1.12': + resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} + + '@tailwindcss/oxide-android-arm64@4.1.12': + resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.12': + resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.12': + resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.12': + resolution: {integrity: sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -485,6 +730,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/emscripten@1.40.1': + resolution: {integrity: sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -500,6 +748,14 @@ packages: '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} + '@types/react-dom@19.1.7': + resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.10': + resolution: {integrity: sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -509,6 +765,12 @@ packages: '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@vitejs/plugin-react@5.0.0': + resolution: {integrity: sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -570,10 +832,18 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caniuse-lite@1.0.30001735: + resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} @@ -590,6 +860,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -625,6 +899,9 @@ packages: resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} engines: {node: '>=12.20.0'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -632,6 +909,9 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -649,9 +929,16 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + electron-to-chromium@1.5.203: + resolution: {integrity: sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -671,6 +958,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + fast-check@4.2.0: + resolution: {integrity: sha512-buxrKEaSseOwFjt6K1REcGMeFOrb0wk3cXifeMAG8yahcE9kV20PjQn1OdzPGL6OBFTbYXfjleNBARf/aCfV1A==} + engines: {node: '>=12.17.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -709,10 +1000,17 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.11.0: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -731,13 +1029,30 @@ packages: is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-bignum@0.0.3: resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} engines: {node: '>=0.8'} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -808,9 +1123,28 @@ packages: loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -833,6 +1167,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -874,9 +1211,29 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + peerDependencies: + react: ^19.1.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -889,6 +1246,13 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -935,6 +1299,17 @@ packages: resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} engines: {node: '>=12.17'} + tailwindcss@4.1.12: + resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -995,9 +1370,23 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uplot@1.6.32: + resolution: {integrity: sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==} + url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1083,6 +1472,11 @@ packages: resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} engines: {node: '>=12.17'} + wouter@3.7.1: + resolution: {integrity: sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==} + peerDependencies: + react: '>=16.8.0' + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -1095,6 +1489,13 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -1107,8 +1508,143 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + zustand@5.0.7: + resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.3': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.3': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + + '@babel/parser@7.28.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@biomejs/biome@2.2.0': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.2.0 @@ -1332,8 +1868,29 @@ snapshots: mute-stream: 1.0.0 optional: true + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.2.3': {} '@lezer/highlight@1.2.1': @@ -1380,6 +1937,8 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 + '@rolldown/pluginutils@1.0.0-beta.30': {} + '@rollup/rollup-android-arm-eabi@4.46.2': optional: true @@ -1444,6 +2003,98 @@ snapshots: dependencies: tslib: 2.8.1 + '@tailwindcss/node@4.1.12': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.12 + + '@tailwindcss/oxide-android-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide@4.1.12': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-x64': 4.1.12 + '@tailwindcss/oxide-freebsd-x64': 4.1.12 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.12 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-x64-musl': 4.1.12 + '@tailwindcss/oxide-wasm32-wasi': 4.1.12 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + + '@tailwindcss/vite@4.1.12(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1))': + dependencies: + '@tailwindcss/node': 4.1.12 + '@tailwindcss/oxide': 4.1.12 + tailwindcss: 4.1.12 + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.2 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.2 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -1457,6 +2108,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/emscripten@1.40.1': {} + '@types/estree@1.0.8': {} '@types/mute-stream@0.0.4': @@ -1477,6 +2130,14 @@ snapshots: dependencies: undici-types: 7.10.0 + '@types/react-dom@19.1.7(@types/react@19.1.10)': + dependencies: + '@types/react': 19.1.10 + + '@types/react@19.1.10': + dependencies: + csstype: 3.1.3 + '@types/statuses@2.0.6': optional: true @@ -1486,6 +2147,18 @@ snapshots: '@types/wrap-ansi@3.0.0': optional: true + '@vitejs/plugin-react@5.0.0(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1))': + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) + '@rolldown/pluginutils': 1.0.0-beta.30 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -1494,14 +2167,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.4.9(typescript@5.9.2))(vite@7.1.2(@types/node@24.3.0)(lightningcss@1.30.1))': + '@vitest/mocker@3.2.4(msw@2.4.9(typescript@5.9.2))(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.4.9(typescript@5.9.2) - vite: 7.1.2(@types/node@24.3.0)(lightningcss@1.30.1) + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -1573,8 +2246,17 @@ snapshots: assertion-error@2.0.1: {} + browserslist@4.25.2: + dependencies: + caniuse-lite: 1.0.30001735 + electron-to-chromium: 1.5.203 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.2) + cac@6.7.14: {} + caniuse-lite@1.0.30001735: {} + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -1594,6 +2276,8 @@ snapshots: check-error@2.1.1: {} + chownr@3.0.0: {} + cli-width@4.1.0: optional: true @@ -1641,23 +2325,33 @@ snapshots: table-layout: 4.1.1 typical: 7.3.0 + convert-source-map@2.0.0: {} + cookie@0.7.2: optional: true crelt@1.0.6: {} + csstype@3.1.3: {} + debug@4.4.1: dependencies: ms: 2.1.3 deep-eql@5.0.2: {} - detect-libc@2.0.4: - optional: true + detect-libc@2.0.4: {} + + electron-to-chromium@1.5.203: {} emoji-regex@8.0.0: optional: true + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + es-module-lexer@1.7.0: {} esbuild@0.25.9: @@ -1689,8 +2383,7 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 - escalade@3.2.0: - optional: true + escalade@3.2.0: {} estree-walker@3.0.3: dependencies: @@ -1698,6 +2391,10 @@ snapshots: expect-type@1.2.2: {} + fast-check@4.2.0: + dependencies: + pure-rand: 7.0.1 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -1718,9 +2415,13 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: optional: true + graceful-fs@4.2.11: {} + graphql@16.11.0: optional: true @@ -1735,10 +2436,18 @@ snapshots: is-node-process@1.2.0: optional: true + jiti@2.5.1: {} + + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + jsesc@3.1.0: {} + json-bignum@0.0.3: {} + json5@2.2.3: {} + lightningcss-darwin-arm64@1.30.1: optional: true @@ -1783,16 +2492,29 @@ snapshots: lightningcss-linux-x64-musl: 1.30.1 lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 - optional: true lodash.camelcase@4.3.0: {} loupe@3.2.0: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mkdirp@3.0.1: {} + ms@2.1.3: {} msw@2.4.9(typescript@5.9.2): @@ -1823,6 +2545,8 @@ snapshots: nanoid@3.3.11: {} + node-releases@2.0.19: {} + outvariant@1.4.3: optional: true @@ -1859,9 +2583,22 @@ snapshots: punycode@2.3.1: optional: true + pure-rand@7.0.1: {} + querystringify@2.2.0: optional: true + react-dom@19.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + scheduler: 0.26.0 + + react-refresh@0.17.0: {} + + react@19.1.1: {} + + regexparam@3.0.0: {} + require-directory@2.1.1: optional: true @@ -1894,6 +2631,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 + scheduler@0.26.0: {} + + semver@6.3.1: {} + siginfo@2.0.0: {} signal-exit@4.1.0: @@ -1938,6 +2679,19 @@ snapshots: array-back: 6.2.2 wordwrapjs: 5.1.0 + tailwindcss@4.1.12: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1982,19 +2736,31 @@ snapshots: universalify@0.2.0: optional: true + update-browserslist-db@1.1.3(browserslist@4.25.2): + dependencies: + browserslist: 4.25.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uplot@1.6.32: {} + url-parse@1.5.10: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 optional: true - vite-node@3.2.4(@types/node@24.3.0)(lightningcss@1.30.1): + use-sync-external-store@1.5.0(react@19.1.1): + dependencies: + react: 19.1.1 + + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.2(@types/node@24.3.0)(lightningcss@1.30.1) + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) transitivePeerDependencies: - '@types/node' - jiti @@ -2009,7 +2775,7 @@ snapshots: - tsx - yaml - vite@7.1.2(@types/node@24.3.0)(lightningcss@1.30.1): + vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2020,13 +2786,14 @@ snapshots: optionalDependencies: '@types/node': 24.3.0 fsevents: 2.3.3 + jiti: 2.5.1 lightningcss: 1.30.1 - vitest@3.2.4(@types/node@24.3.0)(lightningcss@1.30.1)(msw@2.4.9(typescript@5.9.2)): + vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(msw@2.4.9(typescript@5.9.2)): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.4.9(typescript@5.9.2))(vite@7.1.2(@types/node@24.3.0)(lightningcss@1.30.1)) + '@vitest/mocker': 3.2.4(msw@2.4.9(typescript@5.9.2))(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2044,8 +2811,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.2(@types/node@24.3.0)(lightningcss@1.30.1) - vite-node: 3.2.4(@types/node@24.3.0)(lightningcss@1.30.1) + vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.3.0 @@ -2072,6 +2839,13 @@ snapshots: wordwrapjs@5.1.0: {} + wouter@3.7.1(react@19.1.1): + dependencies: + mitt: 3.0.1 + react: 19.1.1 + regexparam: 3.0.0 + use-sync-external-store: 1.5.0(react@19.1.1) + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -2089,6 +2863,10 @@ snapshots: y18n@5.0.8: optional: true + yallist@3.1.1: {} + + yallist@5.0.0: {} + yargs-parser@21.1.1: optional: true @@ -2105,3 +2883,9 @@ snapshots: yoctocolors-cjs@2.1.2: optional: true + + zustand@5.0.7(@types/react@19.1.10)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + optionalDependencies: + '@types/react': 19.1.10 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d726e96..6c964da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,4 @@ onlyBuiltDependencies: + - '@tailwindcss/oxide' + - esbuild - msw diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..f2b5608 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,498 @@ +import { sql } from '@codemirror/lang-sql' +import { EditorState } from '@codemirror/state' +import { showPanel } from '@codemirror/view' +import * as duckdb from '@duckdb/duckdb-wasm' +import duckdb_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?worker' +import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url' +import { vim } from '@replit/codemirror-vim' +import { EditorView, basicSetup } from 'codemirror' +import type React from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' + +const DEFAULT_SQL = `SELECT + time_bucket, + channel_id, + session_id, + connection_id, + bytes_sent_diff, + bytes_received_diff, + packets_sent_diff, + packets_received_diff + FROM ( + SELECT + time_bucket, + channel_id, + session_id, + connection_id, + bytes_sent - LAG(bytes_sent) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS bytes_sent_diff, + bytes_received - LAG(bytes_received) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS bytes_received_diff, + packets_sent - LAG(packets_sent) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS packets_sent_diff, + packets_received - LAG(packets_received) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS packets_received_diff + FROM ( + SELECT + strftime(time_bucket('15 seconds', strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ')), '%Y-%m-%d %H:%M:%S') AS time_bucket, + channel_id, + session_id, + connection_id, + MAX(CAST(rtc_data->'$.bytesSent' AS BIGINT)) AS bytes_sent, + MAX(CAST(rtc_data->'$.bytesReceived' AS BIGINT)) AS bytes_received, + MAX(CAST(rtc_data->'$.packetsSent' AS BIGINT)) AS packets_sent, + MAX(CAST(rtc_data->'$.packetsReceived' AS BIGINT)) AS packets_received + FROM rtc_stats + WHERE rtc_type = 'transport' + GROUP BY time_bucket, channel_id, session_id, connection_id + ) + ) + WHERE + bytes_sent_diff IS NOT NULL AND + bytes_received_diff IS NOT NULL AND + packets_sent_diff IS NOT NULL AND + packets_received_diff IS NOT NULL + ORDER BY time_bucket ASC;` + +const FILE_NAME = 'rtc_stats.parquet' + +function App() { + const [db, setDb] = useState(null) + const [isVimMode, setIsVimMode] = useState(true) + const [opfsStatus, setOpfsStatus] = useState(false) + const [counted, setCounted] = useState(0) + const [duckdbVersion, setDuckdbVersion] = useState('') + const [duckdbWasmVersion, setDuckdbWasmVersion] = useState('') + const [buttonsEnabled, setButtonsEnabled] = useState(false) + const [fetchButtonEnabled, setFetchButtonEnabled] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const [resultHTML, setResultHTML] = useState('') + + const editorRef = useRef(null) + const editorViewRef = useRef(null) + + const getParquetBuffer = useCallback(async (PARQUET_FILE_URL: string): Promise => { + const response = await fetch(PARQUET_FILE_URL) + return response.arrayBuffer() + }, []) + + const readParquetFile = useCallback( + async (db: duckdb.AsyncDuckDB, buffer: ArrayBuffer): Promise => { + const conn = await db.connect() + try { + const tableExists = await conn.query(` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_name = 'rtc_stats' + ) as exists_flag; + `) + + const exists = JSON.parse(tableExists.toArray()[0] as unknown as string).exists_flag + + if (!exists) { + console.log('テーブルが存在しないため作成します') + await db.registerFileBuffer(`${FILE_NAME}`, new Uint8Array(buffer)) + await conn.query(` + CREATE TABLE rtc_stats AS SELECT * FROM read_parquet('${FILE_NAME}'); + `) + } else { + console.log('テーブルは既に存在します') + } + setFetchButtonEnabled(false) + } catch (error) { + console.error('テーブル作成中にエラーが発生しました:', error) + throw error + } finally { + console.log('close') + await db.dropFile(`${FILE_NAME}`) + await conn.close() + } + }, + [], + ) + + const performSearch = useCallback( + async (db: duckdb.AsyncDuckDB, searchTerm: string): Promise => { + if (!searchTerm.trim()) { + setResultHTML('') + return + } + + const conn = await db.connect() + const result = await conn.query(` + SELECT timestamp, connection_id, rtc_type + FROM rtc_stats + WHERE connection_id LIKE '%${searchTerm}%' + OR channel_id LIKE '%${searchTerm}%' + OR timestamp LIKE '%${searchTerm}%' + OR rtc_type LIKE '%${searchTerm}%' + USING SAMPLE 1 PERCENT (bernoulli); + `) + + const headers = ['timestamp', 'connection_id', 'rtc_type'] + const rows = result.toArray().map((row: unknown) => { + const parsedRow = JSON.parse(row as string) + return headers.map((header) => `${parsedRow[header]}`).join('') + }) + + const tableHTML = ` + + ${headers.map((h) => ``).join('')} + ${rows.map((r: string) => `${r}`).join('')} +
${h}
+ ` + setResultHTML(tableHTML) + + await conn.close() + }, + [], + ) + + const initializeDb = useCallback(async () => { + // biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation for index signatures + const PARQUET_FILE_URL = import.meta.env['VITE_PARQUET_FILE_URL'] + + const worker = new duckdb_worker() + const logger = new duckdb.ConsoleLogger() + const newDb = new duckdb.AsyncDuckDB(logger, worker) + + await newDb.instantiate(duckdb_wasm) + await newDb.open({ + path: 'opfs://duckdb-wasm-parquet.db', + accessMode: duckdb.DuckDBAccessMode.READ_WRITE, + }) + + setDb(newDb) + + const version = await newDb.getVersion() + setDuckdbVersion(`DuckDB: ${version}`) + setDuckdbWasmVersion(`DuckDB-Wasm: ${duckdb.PACKAGE_VERSION}`) + + const conn = await newDb.connect() + await conn.query(` + INSTALL parquet; + LOAD parquet; + INSTALL json; + LOAD json; + `) + await conn.close() + + const buffer = await getParquetBuffer(PARQUET_FILE_URL) + await readParquetFile(newDb, buffer) + + setOpfsStatus(true) + + const conn2 = await newDb.connect() + const result = await conn2.query(` + SELECT COUNT(*) FROM rtc_stats; + `) + const count = JSON.parse(result.toArray()[0] as unknown as string)['count_star()'] + setCounted(count) + await conn2.close() + + setButtonsEnabled(true) + }, [getParquetBuffer, readParquetFile]) + + const initEditor = useCallback( + (db: duckdb.AsyncDuckDB) => { + if (!editorRef.current || editorViewRef.current) return + + const createExtensions = (isVim: boolean) => [ + isVim + ? vim({ + status: true, + }) + : [], + sql(), + basicSetup, + EditorState.readOnly.of(false), + EditorView.lineWrapping, + EditorView.updateListener.of((update) => { + if ( + update.docChanged && + update.state.doc.toString() === '' && + update.transactions.every((tr) => !tr.isUserEvent('input') && !tr.isUserEvent('delete')) + ) { + editorViewRef.current?.dispatch({ + changes: { + from: 0, + to: 0, + insert: DEFAULT_SQL, + }, + }) + } + }), + showPanel.of((view) => { + const dom = document.createElement('div') + dom.style.cssText = ` + padding: 4px; + display: flex; + justify-content: flex-end; + background: #f5f5f5; + gap: 8px; + ` + + const vimToggleButton = document.createElement('button') + vimToggleButton.textContent = isVimMode ? 'Normal Mode' : 'Vim Mode' + vimToggleButton.style.cssText = ` + padding: 5px 10px; + background: #2196F3; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + ` + vimToggleButton.addEventListener('click', () => toggleVimMode()) + + const runButton = document.createElement('button') + runButton.textContent = 'Run Query' + runButton.style.cssText = ` + padding: 5px 10px; + background: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + ` + runButton.addEventListener('click', async () => { + const query = view.state.doc.toString() + const conn = await db.connect() + try { + const result = await conn.query(query) + const rows = result.toArray() + if (rows.length > 0) { + const headers = Object.keys(JSON.parse(rows[0] as unknown as string)) + const tableHTML = ` + + ${headers.map((h) => ``).join('')} + ${rows + .map((row: unknown) => { + const parsedRow = JSON.parse(row as string) + return `${headers.map((h) => ``).join('')}` + }) + .join('')} +
${h}
${parsedRow[h]}
+ ` + setResultHTML(tableHTML) + } + } catch (error) { + console.error('Query execution error:', error) + } finally { + await conn.close() + } + }) + + dom.appendChild(vimToggleButton) + dom.appendChild(runButton) + return { dom, bottom: true } + }), + EditorView.theme({ + '&': { + height: '400px', + maxWidth: '800px', + position: 'relative', + marginBottom: '20px', + }, + '.cm-scroller': { + overflow: 'auto', + }, + }), + ] + + editorViewRef.current = new EditorView({ + state: EditorState.create({ + doc: DEFAULT_SQL, + extensions: createExtensions(isVimMode), + }), + }) + + editorRef.current.appendChild(editorViewRef.current.dom) + + const toggleVimMode = () => { + setIsVimMode((prev) => !prev) + const currentSQL = editorViewRef.current?.state.doc.toString() || DEFAULT_SQL + editorViewRef.current?.setState( + EditorState.create({ + doc: currentSQL, + extensions: createExtensions(!isVimMode), + }), + ) + } + }, + [isVimMode], + ) + + useEffect(() => { + initializeDb() + }, [initializeDb]) + + useEffect(() => { + if (db && editorRef.current && !editorViewRef.current) { + initEditor(db) + } + }, [db, initEditor]) + + useEffect(() => { + if (db && searchTerm) { + performSearch(db, searchTerm) + } + }, [searchTerm, db, performSearch]) + + const handleFetchParquet = async () => { + await initializeDb() + } + + const handleSamples = async () => { + if (!db) return + const conn = await db.connect() + const result = await conn.query(` + SELECT timestamp, connection_id, rtc_type + FROM rtc_stats + USING SAMPLE 1 PERCENT (bernoulli); + `) + + const headers = ['timestamp', 'connection_id', 'rtc_type'] + const rows = result.toArray().map((row: unknown) => { + const parsedRow = JSON.parse(row as string) + return headers.map((header) => `${parsedRow[header]}`).join('') + }) + + const tableHTML = ` + + ${headers.map((h) => ``).join('')} + ${rows.map((r: string) => `${r}`).join('')} +
${h}
+ ` + setResultHTML(tableHTML) + + await conn.close() + } + + const handleSamplesDownloadParquet = async () => { + if (!db) return + const conn = await db.connect() + try { + await conn.query(` + COPY (SELECT * FROM rtc_stats + USING SAMPLE 1 PERCENT (bernoulli)) TO samples.parquet (FORMAT 'parquet', COMPRESSION 'zstd'); + `) + const parquet_buffer = await db.copyFileToBuffer('samples.parquet') + const blob = new Blob([parquet_buffer as unknown as ArrayBuffer], { + type: 'application/octet-stream', + }) + + const downloadUrl = URL.createObjectURL(blob) + const downloadLink = document.createElement('a') + downloadLink.href = downloadUrl + downloadLink.download = 'samples.parquet' + document.body.appendChild(downloadLink) + downloadLink.click() + document.body.removeChild(downloadLink) + + URL.revokeObjectURL(downloadUrl) + } catch (error) { + console.error('Parquetファイルのダウンロード中にエラーが発生しました:', error) + } finally { + await conn.close() + } + } + + const handleAggregation = async () => { + if (!db) return + const conn = await db.connect() + const result = await conn.query(DEFAULT_SQL) + + const headers = [ + 'time_bucket', + 'channel_id', + 'session_id', + 'connection_id', + 'bytes_sent_diff', + 'bytes_received_diff', + 'packets_sent_diff', + 'packets_received_diff', + ] + + const rows = result.toArray().map((row: unknown) => { + const parsedRow = JSON.parse(row as string) + return headers.map((header) => `${parsedRow[header]}`).join('') + }) + + const tableHTML = ` + + ${headers.map((h) => ``).join('')} + ${rows.map((r: string) => `${r}`).join('')} +
${h}
+ ` + setResultHTML(tableHTML) + + await conn.close() + } + + const handlePurge = async () => { + if (!db) return + setResultHTML('') + + await db.terminate() + const opfsRoot = await navigator.storage.getDirectory() + await opfsRoot.removeEntry('duckdb-wasm-parquet.db').catch(() => {}) + await opfsRoot.removeEntry('duckdb-wasm-parquet.db.wal').catch(() => {}) + + setOpfsStatus(false) + setCounted(0) + setFetchButtonEnabled(true) + setButtonsEnabled(false) + setDb(null) + } + + return ( +
+

DuckDB-Wasm (OPFS) + Parquet + S3-compatible object storage

+

+ GitHub:{' '} + + https://github.com/voluntas/duckdb-wasm-parquet + +

+

{duckdbVersion}

+

{duckdbWasmVersion}

+

OPFS: {opfsStatus ? 'true' : 'false'}

+

Counted: {counted}

+
+ + + + + +
+ ) => setSearchTerm(e.target.value)} + disabled={!buttonsEnabled} + style={{ + width: '50%', + maxWidth: '500px', + padding: '10px', + marginBottom: '10px', + fontSize: '16px', + border: '1px solid #ddd', + borderRadius: '4px', + display: 'inline-block', + }} + /> + {/* biome-ignore lint/security/noDangerouslySetInnerHtml: テーブル表示のため */} +
+
+ ) +} + +export default App diff --git a/src/main.css b/src/main.css new file mode 100644 index 0000000..8e414c6 --- /dev/null +++ b/src/main.css @@ -0,0 +1,35 @@ +@import "tailwindcss" source("."); + +#result { + margin-top: 20px; + font-family: Arial, sans-serif; +} + +#result table { + border-collapse: collapse; + border: 2px solid #ddd; + table-layout: auto; + width: auto; +} + +#result th, +#result td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + white-space: nowrap; +} + +#result th { + background-color: #f2f2f2; + font-weight: bold; + border-bottom: 2px solid #ddd; +} + +#result tr:nth-child(even) { + background-color: #f9f9f9; +} + +#result tr:hover { + background-color: #f5f5f5; +} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 576032c..0000000 --- a/src/main.ts +++ /dev/null @@ -1,698 +0,0 @@ -import { sql } from '@codemirror/lang-sql' -import { EditorState } from '@codemirror/state' -import { showPanel } from '@codemirror/view' -import * as duckdb from '@duckdb/duckdb-wasm' -import duckdb_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?worker' -import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url' -import { vim } from '@replit/codemirror-vim' -import { EditorView, basicSetup } from 'codemirror' - -let isVimMode = true -let editor: EditorView - -document.addEventListener('DOMContentLoaded', async () => { - const PARQUET_FILE_URL = import.meta.env.VITE_PARQUET_FILE_URL - const fetchParquetElement = document.querySelector('#fetch-parquet') - const samplesButton = document.querySelector('#samples') - const samplesDownloadParquetButton = document.querySelector( - '#samples-download-parquet', - ) - const aggregationButton = document.querySelector('#aggregation') - const purgeButton = document.querySelector('#purge') - const searchInput = document.querySelector('#search') - - // ボタンを無効化 - if (fetchParquetElement) { - fetchParquetElement.disabled = true - } - if (samplesButton) { - samplesButton.disabled = true - } - if (aggregationButton) { - aggregationButton.disabled = true - } - if (samplesDownloadParquetButton) { - samplesDownloadParquetButton.disabled = true - } - if (purgeButton) { - purgeButton.disabled = true - } - if (searchInput) { - searchInput.disabled = true - } - - const DEFAULT_SQL = `SELECT - time_bucket, - channel_id, - session_id, - connection_id, - bytes_sent_diff, - bytes_received_diff, - packets_sent_diff, - packets_received_diff - FROM ( - SELECT - time_bucket, - channel_id, - session_id, - connection_id, - bytes_sent - LAG(bytes_sent) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS bytes_sent_diff, - bytes_received - LAG(bytes_received) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS bytes_received_diff, - packets_sent - LAG(packets_sent) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS packets_sent_diff, - packets_received - LAG(packets_received) OVER (PARTITION BY channel_id, session_id, connection_id ORDER BY time_bucket) AS packets_received_diff - FROM ( - SELECT - strftime(time_bucket('15 seconds', strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ')), '%Y-%m-%d %H:%M:%S') AS time_bucket, - channel_id, - session_id, - connection_id, - MAX(CAST(rtc_data->'$.bytesSent' AS BIGINT)) AS bytes_sent, - MAX(CAST(rtc_data->'$.bytesReceived' AS BIGINT)) AS bytes_received, - MAX(CAST(rtc_data->'$.packetsSent' AS BIGINT)) AS packets_sent, - MAX(CAST(rtc_data->'$.packetsReceived' AS BIGINT)) AS packets_received - FROM rtc_stats - WHERE rtc_type = 'transport' - GROUP BY time_bucket, channel_id, session_id, connection_id - ) - ) - WHERE - bytes_sent_diff IS NOT NULL AND - bytes_received_diff IS NOT NULL AND - packets_sent_diff IS NOT NULL AND - packets_received_diff IS NOT NULL - ORDER BY time_bucket ASC;` - - editor = new EditorView({ - state: EditorState.create({ - doc: DEFAULT_SQL, - extensions: [ - isVimMode - ? vim({ - status: true, - }) - : [], - sql(), - basicSetup, - EditorState.readOnly.of(false), - EditorView.lineWrapping, - EditorView.updateListener.of((update) => { - if ( - update.docChanged && - update.state.doc.toString() === '' && - update.transactions.every((tr) => !tr.isUserEvent('input') && !tr.isUserEvent('delete')) - ) { - editor.dispatch({ - changes: { - from: 0, - to: 0, - insert: DEFAULT_SQL, - }, - }) - } - }), - showPanel.of((view) => { - const dom = document.createElement('div') - dom.style.cssText = ` - padding: 4px; - display: flex; - justify-content: flex-end; - background: #f5f5f5; - gap: 8px; - ` - - // Vim mode トグルボタン - const vimToggleButton = document.createElement('button') - vimToggleButton.textContent = isVimMode ? 'Normal Mode' : 'Vim Mode' - vimToggleButton.style.cssText = ` - padding: 5px 10px; - background: #2196F3; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - ` - vimToggleButton.addEventListener('click', () => toggleVimMode(db)) - - // 実行ボタン - const runButton = document.createElement('button') - runButton.textContent = 'Run Query' - runButton.style.cssText = ` - padding: 5px 10px; - background: #4CAF50; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - ` - runButton.addEventListener('click', async () => { - const query = view.state.doc.toString() - const conn = await db.connect() - try { - const result = await conn.query(query) - const resultElement = document.getElementById('result') - if (resultElement) { - const table = document.createElement('table') - const rows = result.toArray() - if (rows.length > 0) { - const headers = Object.keys(JSON.parse(rows[0])) - - const headerRow = document.createElement('tr') - headerRow.innerHTML = headers.map((header) => `${header}`).join('') - table.appendChild(headerRow) - - for (const row of rows) { - const parsedRow = JSON.parse(row) - const tr = document.createElement('tr') - tr.innerHTML = headers.map((header) => `${parsedRow[header]}`).join('') - table.appendChild(tr) - } - - resultElement.innerHTML = '' - resultElement.appendChild(table) - } - } - } catch (error) { - console.error('Query execution error:', error) - } finally { - await conn.close() - } - }) - - dom.appendChild(vimToggleButton) - dom.appendChild(runButton) - return { dom, bottom: true } - }), - EditorView.theme({ - '&': { - height: '400px', - maxWidth: '800px', - position: 'relative', - marginBottom: '20px', - }, - '.cm-scroller': { - overflow: 'auto', - }, - }), - ], - }), - }) - - const editorElement = document.getElementById('editor') - if (editorElement) { - editorElement.appendChild(editor.dom) - } - - // すべてのボタンを初期状態で無効化 - for (const button of [ - samplesButton, - samplesDownloadParquetButton, - aggregationButton, - purgeButton, - searchInput, - ]) { - if (button) button.disabled = true - } - - if (searchInput) { - searchInput.addEventListener('input', async () => { - await performSearch(db, searchInput.value) - }) - } - - let worker = new duckdb_worker() - let logger = new duckdb.ConsoleLogger() - let db = new duckdb.AsyncDuckDB(logger, worker) - - await db.instantiate(duckdb_wasm) - await db.open({ - path: 'opfs://duckdb-wasm-parquet.db', - accessMode: duckdb.DuckDBAccessMode.READ_WRITE, - }) - - const duckdbVersionElement = document.getElementById('duckdb-version') - if (duckdbVersionElement) { - const version = await db.getVersion() - duckdbVersionElement.textContent = `DuckDB: ${version}` - } - - const duckdbWasmVersionElement = document.getElementById('duckdb-wasm-version') - if (duckdbWasmVersionElement) { - const version = duckdb.PACKAGE_VERSION - duckdbWasmVersionElement.textContent = `DuckDB-Wasm: ${version}` - } - - const conn = await db.connect() - await conn.query(` - INSTALL parquet; - LOAD parquet; - INSTALL json; - LOAD json; - `) - await conn.close() - - const buffer = await getParquetBuffer(PARQUET_FILE_URL) - await readParquetFile(db, buffer) - - const opfsStatusElement = document.getElementById('opfsStatus') - if (opfsStatusElement) { - opfsStatusElement.textContent = 'OPFS: true' - } - - const countedElement = document.getElementById('counted') - if (countedElement) { - const conn = await db.connect() - const result = await conn.query(` - SELECT COUNT(*) FROM rtc_stats; - `) - const count = JSON.parse(result.toArray()[0])['count_star()'] - countedElement.textContent = `Counted: ${count}` - await conn.close() - } - - if (samplesButton) { - samplesButton.disabled = false - } - if (aggregationButton) { - aggregationButton.disabled = false - } - if (samplesDownloadParquetButton) { - samplesDownloadParquetButton.disabled = false - } - if (purgeButton) { - purgeButton.disabled = false - } - if (searchInput) { - searchInput.disabled = false - } - - document.getElementById('fetch-parquet')?.addEventListener('click', async () => { - worker = new duckdb_worker() - logger = new duckdb.ConsoleLogger() - db = new duckdb.AsyncDuckDB(logger, worker) - await db.instantiate(duckdb_wasm) - await db.open({ - path: 'opfs://duckdb-wasm-parquet.db', - accessMode: duckdb.DuckDBAccessMode.READ_WRITE, - }) - - const conn = await db.connect() - await conn.query(` - INSTALL parquet; - LOAD parquet; - INSTALL json; - LOAD json; - `) - await conn.close() - - const buffer = await getParquetBuffer(PARQUET_FILE_URL) - await readParquetFile(db, buffer) - - const opfsStatusElement = document.getElementById('opfsStatus') - if (opfsStatusElement) { - opfsStatusElement.textContent = 'OPFS: true' - } - - const countedElement = document.getElementById('counted') - if (countedElement) { - const conn = await db.connect() - const result = await conn.query(` - SELECT COUNT(*) FROM rtc_stats; - `) - const count = JSON.parse(result.toArray()[0])['count_star()'] - countedElement.textContent = `Counted: ${count}` - await conn.close() - } - - const fetchParquetElement = document.querySelector('#fetch-parquet') - if (fetchParquetElement) { - fetchParquetElement.disabled = true - } - - if (samplesButton) { - samplesButton.disabled = false - } - if (samplesDownloadParquetButton) { - samplesDownloadParquetButton.disabled = false - } - if (aggregationButton) { - aggregationButton.disabled = false - } - if (purgeButton) { - purgeButton.disabled = false - } - if (searchInput) { - searchInput.disabled = false - } - }) - - document.getElementById('samples')?.addEventListener('click', async () => { - const conn = await db.connect() - const result = await conn.query(` - SELECT timestamp, connection_id, rtc_type - FROM rtc_stats - USING SAMPLE 1 PERCENT (bernoulli); - `) - - const resultElement = document.getElementById('result') - if (resultElement) { - const table = document.createElement('table') - const headers = ['timestamp', 'connection_id', 'rtc_type'] - - const headerRow = document.createElement('tr') - headerRow.innerHTML = headers.map((header) => `${header}`).join('') - table.appendChild(headerRow) - - const rows = result.toArray().map((row) => { - const parsedRow = JSON.parse(row) - const tr = document.createElement('tr') - tr.innerHTML = headers.map((header) => `${parsedRow[header]}`).join('') - return tr - }) - - table.append(...rows) - - resultElement.innerHTML = '' - resultElement.appendChild(table) - } - - await conn.close() - }) - - document.getElementById('samples-download-parquet')?.addEventListener('click', async () => { - const conn = await db.connect() - try { - await conn.query(` - COPY (SELECT * FROM rtc_stats - USING SAMPLE 1 PERCENT (bernoulli)) TO samples.parquet (FORMAT 'parquet', COMPRESSION 'zstd'); - `) - const parquet_buffer = await db.copyFileToBuffer('samples.parquet') - const blob = new Blob([parquet_buffer], { type: 'application/octet-stream' }) - - // ダウンロードを自動的に開始 - const downloadUrl = URL.createObjectURL(blob) - const downloadLink = document.createElement('a') - downloadLink.href = downloadUrl - downloadLink.download = 'samples.parquet' - document.body.appendChild(downloadLink) - downloadLink.click() // プログラムによる自動クリック - document.body.removeChild(downloadLink) - - // 使用後にURLを解放 - URL.revokeObjectURL(downloadUrl) - } catch (error) { - console.error('Parquetファイルのダウンロード中にエラーが発生しました:', error) - } finally { - await conn.close() - } - }) - - document.getElementById('aggregation')?.addEventListener('click', async () => { - const conn = await db.connect() - // SQL は適当ですので参考にしないで下さい - const result = await conn.query(DEFAULT_SQL) - - const resultElement = document.getElementById('result') - if (resultElement) { - const table = document.createElement('table') - const headers = [ - 'time_bucket', - 'channel_id', - 'session_id', - 'connection_id', - 'bytes_sent_diff', - 'bytes_received_diff', - 'packets_sent_diff', - 'packets_received_diff', - ] - - const headerRow = document.createElement('tr') - headerRow.innerHTML = headers.map((header) => `${header}`).join('') - table.appendChild(headerRow) - - const rows = result.toArray().map((row) => { - const parsedRow = JSON.parse(row) - const tr = document.createElement('tr') - tr.innerHTML = headers.map((header) => `${parsedRow[header]}`).join('') - return tr - }) - - table.append(...rows) - - resultElement.innerHTML = '' - resultElement.appendChild(table) - } - }) - - document.getElementById('purge')?.addEventListener('click', async () => { - const resultElement = document.getElementById('result') - if (resultElement) { - resultElement.innerHTML = '' - } - - await db.terminate() - const opfsRoot = await navigator.storage.getDirectory() - await opfsRoot.removeEntry('duckdb-wasm-parquet.db').catch(() => {}) - await opfsRoot.removeEntry('duckdb-wasm-parquet.db.wal').catch(() => {}) - - const opfsStatusElement = document.getElementById('opfsStatus') - if (opfsStatusElement) { - opfsStatusElement.textContent = 'OPFS: false' - } - - const countedElement = document.getElementById('counted') - if (countedElement) { - countedElement.textContent = 'Counted: 0' - } - - if (fetchParquetElement) { - fetchParquetElement.disabled = false - } - - // ボタンの状態を更新 - if (samplesButton) { - samplesButton.disabled = true - } - if (aggregationButton) { - aggregationButton.disabled = true - } - if (samplesDownloadParquetButton) { - samplesDownloadParquetButton.disabled = true - } - if (purgeButton) { - purgeButton.disabled = true - } - if (searchInput) { - searchInput.disabled = true - } - }) -}) - -// 検索を実行する関数 -const performSearch = async (db: duckdb.AsyncDuckDB, searchTerm: string): Promise => { - const resultElement = document.getElementById('result') - if (!resultElement) return - - // 検索語が空の場合、結果クリアして終了 - if (!searchTerm.trim()) { - resultElement.innerHTML = '' - return - } - - const conn = await db.connect() - const result = await conn.query(` - SELECT timestamp, connection_id, rtc_type - FROM rtc_stats - WHERE connection_id LIKE '%${searchTerm}%' - OR channel_id LIKE '%${searchTerm}%' - OR timestamp LIKE '%${searchTerm}%' - OR rtc_type LIKE '%${searchTerm}%' - USING SAMPLE 1 PERCENT (bernoulli); - `) - - const table = document.createElement('table') - const headers = ['timestamp', 'connection_id', 'rtc_type'] - - const headerRow = document.createElement('tr') - headerRow.innerHTML = headers.map((header) => `${header}`).join('') - table.appendChild(headerRow) - - const rows = result.toArray().map((row) => { - const parsedRow = JSON.parse(row) - const tr = document.createElement('tr') - tr.innerHTML = headers.map((header) => `${parsedRow[header]}`).join('') - return tr - }) - - table.append(...rows) - - resultElement.innerHTML = '' - resultElement.appendChild(table) - - await conn.close() -} - -const FILE_NAME = 'rtc_stats.parquet' - -const readParquetFile = async (db: duckdb.AsyncDuckDB, buffer: ArrayBuffer): Promise => { - const conn = await db.connect() - try { - // テーブルの存在確認をより安全な方法で実装 - const tableExists = await conn.query(` - SELECT EXISTS ( - SELECT 1 - FROM information_schema.tables - WHERE table_name = 'rtc_stats' - ) as exists_flag; - `) - - const exists = tableExists.toArray()[0].exists_flag - - if (!exists) { - console.log('テーブルが存在しないため作成します') - await db.registerFileBuffer(`${FILE_NAME}`, new Uint8Array(buffer)) - await conn.query(` - CREATE TABLE rtc_stats AS SELECT * FROM read_parquet('${FILE_NAME}'); - `) - } else { - console.log('テーブルは既に存在します') - } - const readParquetElement = document.querySelector('#fetch-parquet') - if (readParquetElement) { - readParquetElement.disabled = true - } - } catch (error) { - console.error('テーブル作成中にエラーが発生しました:', error) - throw error - } finally { - console.log('close') - await db.dropFile(`${FILE_NAME}`) - await conn.close() - } -} - -const getParquetBuffer = async (PARQUET_FILE_URL: string): Promise => { - const response = await fetch(PARQUET_FILE_URL) - return response.arrayBuffer() -} - -// トグルボタンを追加 -const toggleVimMode = (db: duckdb.AsyncDuckDB) => { - isVimMode = !isVimMode - - // エディタの状態を再構築 - const currentSQL = editor.state.doc.toString() - editor.setState( - EditorState.create({ - doc: currentSQL, - extensions: [ - isVimMode - ? vim({ - status: true, - }) - : [], - sql(), - basicSetup, - EditorState.readOnly.of(false), - EditorView.lineWrapping, - EditorView.updateListener.of((update) => { - if ( - update.docChanged && - update.state.doc.toString() === '' && - update.transactions.every((tr) => !tr.isUserEvent('input') && !tr.isUserEvent('delete')) - ) { - editor.dispatch({ - changes: { - from: 0, - to: 0, - insert: currentSQL, - }, - }) - } - }), - showPanel.of((view) => { - const dom = document.createElement('div') - dom.style.cssText = ` - padding: 4px; - display: flex; - justify-content: flex-end; - background: #f5f5f5; - gap: 8px; - ` - - // Vim mode トグルボタン - const vimToggleButton = document.createElement('button') - vimToggleButton.textContent = isVimMode ? 'Normal Mode' : 'Vim Mode' - vimToggleButton.style.cssText = ` - padding: 5px 10px; - background: #2196F3; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - ` - vimToggleButton.addEventListener('click', () => toggleVimMode(db)) - - // 実行ボタン - const runButton = document.createElement('button') - runButton.textContent = 'Run Query' - runButton.style.cssText = ` - padding: 5px 10px; - background: #4CAF50; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - ` - runButton.addEventListener('click', async () => { - const query = view.state.doc.toString() - const conn = await db.connect() - try { - const result = await conn.query(query) - const resultElement = document.getElementById('result') - if (resultElement) { - const table = document.createElement('table') - const rows = result.toArray() - if (rows.length > 0) { - const headers = Object.keys(JSON.parse(rows[0])) - - const headerRow = document.createElement('tr') - headerRow.innerHTML = headers.map((header) => `${header}`).join('') - table.appendChild(headerRow) - - for (const row of rows) { - const parsedRow = JSON.parse(row) - const tr = document.createElement('tr') - tr.innerHTML = headers.map((header) => `${parsedRow[header]}`).join('') - table.appendChild(tr) - } - - resultElement.innerHTML = '' - resultElement.appendChild(table) - } - } - } catch (error) { - console.error('Query execution error:', error) - } finally { - await conn.close() - } - }) - - dom.appendChild(vimToggleButton) - dom.appendChild(runButton) - return { dom, bottom: true } - }), - EditorView.theme({ - '&': { - height: '400px', - maxWidth: '800px', - position: 'relative', - marginBottom: '20px', - }, - '.cm-scroller': { - overflow: 'auto', - }, - }), - ], - }), - ) -} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..79401d1 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './main.css' + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +ReactDOM.createRoot(rootElement).render( + + + , +) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..c10c651 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,6 @@ +import type { Config } from 'tailwindcss' + +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + plugins: [], +} satisfies Config diff --git a/tsconfig.json b/tsconfig.json index e6799fc..f4e5ae5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,99 @@ { "compilerOptions": { - "target": "ESNext", + /* 言語と環境 */ + // コンパイル後のJavaScriptのバージョン + "target": "ES2020", + // 使用可能な標準ライブラリ + "lib": ["ES2020", "DOM", "DOM.Iterable"], + // モジュールシステムの種類 "module": "ESNext", + // モジュール解決方法(バンドラー向け) + "moduleResolution": "bundler", + // JSX変換モード(React 17+の新しい変換) + "jsx": "react-jsx", + + /* 厳格な型チェックオプション */ + // 全ての厳格な型チェックオプションを有効化 "strict": true, - "declaration": true, + // 暗黙的なany型を禁止 + "noImplicitAny": true, + // null/undefined のチェックを厳格化 "strictNullChecks": true, - "importHelpers": true, - "moduleResolution": "Bundler", - "experimentalDecorators": true, + // 関数の型チェックを厳格化 + "strictFunctionTypes": true, + // bind/call/apply の型チェックを厳格化 + "strictBindCallApply": true, + // クラスプロパティの初期化を必須化 + "strictPropertyInitialization": true, + // 暗黙的なthis型を禁止 + "noImplicitThis": true, + // catch変数をunknown型として扱う + "useUnknownInCatchVariables": true, + // 常に"use strict"を出力 + "alwaysStrict": true, + + /* 追加のチェック */ + // 未使用のローカル変数を禁止 + "noUnusedLocals": true, + // 未使用の関数パラメータを禁止 + "noUnusedParameters": true, + // オプショナルプロパティの型を厳密化 + "exactOptionalPropertyTypes": true, + // 暗黙的なreturnを禁止 + "noImplicitReturns": true, + // switchのフォールスルーを禁止 + "noFallthroughCasesInSwitch": true, + // インデックスアクセスの結果にundefinedを含める + "noUncheckedIndexedAccess": true, + // overrideキーワードの省略を禁止 + "noImplicitOverride": true, + // インデックスシグネチャからのプロパティアクセスを禁止 + "noPropertyAccessFromIndexSignature": true, + // 未使用のラベルを禁止 + "allowUnusedLabels": false, + // 到達不可能なコードを禁止 + "allowUnreachableCode": false, + + /* モジュール解決オプション */ + // CommonJSモジュールとの相互運用性を向上 "esModuleInterop": true, - "allowSyntheticDefaultImports": true, + // 型定義ファイルも含めて全て型チェック(厳格モード) + "skipLibCheck": false, + // ファイル名の大文字小文字を厳密に区別 + "forceConsistentCasingInFileNames": true, + // JSONファイルのインポートを許可 "resolveJsonModule": true, - "stripInternal": true, - "newLine": "LF", - "types": [], - "lib": ["esnext", "dom", "dom.iterable", "scripthost"] + // 各ファイルを独立したモジュールとして扱う + "isolatedModules": true, + + /* 出力オプション */ + // 型定義ファイル(.d.ts)を生成 + "declaration": true, + // 型定義ファイルのソースマップを生成 + "declarationMap": true, + // ソースマップを生成 + "sourceMap": true, + // 出力先ディレクトリ + "outDir": "dist", + // ソースコードのルートディレクトリ + "rootDir": "src", + // ファイルを出力しない(型チェックのみ) + "noEmit": true, + // コメントを削除 + "removeComments": true, + // 改行コードをLFに統一 + "newLine": "lf", + + /* 高度なオプション */ + // デフォルトインポートの合成を許可 + "allowSyntheticDefaultImports": true, + // const enumを保持 + "preserveConstEnums": true, + // @internalタグが付いた宣言を削除 + "stripInternal": true }, - "exclude": ["node_modules", "tests"] -} \ No newline at end of file + // 対象ファイル + "include": ["src/**/*"], + // 除外ファイル + "exclude": ["node_modules", "dist", "playwright-ct.config.ts", "src/**/*.ct.tsx"] +} diff --git a/vite.config.ts b/vite.config.ts index 68164e5..5aa0ea5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,20 @@ +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' export default defineConfig({ + plugins: [react(), tailwindcss()], base: process.env.NODE_ENV === 'production' ? '/duckdb-wasm-parquet/' : '/', + server: { + port: 5173, + host: true, + }, + optimizeDeps: { + include: ['uplot'], + }, + build: { + rollupOptions: { + external: [], + }, + }, })