diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 63adca9..919f337 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,8 @@ permissions: env: PKG_NAME: npm-package jobs: - build: + prepare: + if: ${{github.repository_owner == 'asnowc'}} runs-on: ubuntu-latest outputs: version: ${{fromJson(env.PACKAGE_JSON).version}} @@ -54,7 +55,7 @@ jobs: path: vio/assets/ retention-days: 7 publish-npm: - needs: build + needs: prepare runs-on: ubuntu-latest steps: - name: Download @@ -75,7 +76,7 @@ jobs: publish-jsr: runs-on: ubuntu-latest - needs: build + needs: prepare steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9496747..a892c4c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,10 +1,11 @@ -name: Test +name: Unit Test on: push: branches: - main - - feat/* + - dev - test/* + - feat/* jobs: test: runs-on: ubuntu-latest @@ -19,8 +20,24 @@ jobs: run_install: true - name: Unit test run: pnpm run ci:test + - name: Type check + run: pnpm run type-check + e2e: + needs: test + if: ${{!startsWith(github.ref_name,'feat/')}} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup NodeJS + uses: asnowc/actions@setup-node/v2 + with: + node_v: 22 + pnpm_v: 9 + run_install: true - name: Build run: pnpm run -r ci:build + - name: Install browser run: pnpm playwright install chromium webkit firefox --with-deps working-directory: ./e2e @@ -36,4 +53,4 @@ jobs: path: e2e/playwright-report/ - name: Result if: ${{steps.e2e.outcome=='failure'}} - run: exit 1 + run: exit diff --git a/.vscode/settings.json b/.vscode/settings.json index 90d9da2..001bd07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { - "deno.enable": true, - "deno.config": "./vio/deno.json", - "deno.enablePaths": ["./scripts", "./vio/src/mod_deno.ts"], + "deno.enablePaths": ["./scripts", "./vio/src/mod.deno.ts"], "cSpell.words": ["asla", "cpcall", "dockview", "echarts"], "code-runner.executorMap": { "javascript": "node", diff --git a/CHANGELOG.md b/CHANGELOG.md index 474c1b8..392cd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,28 @@ -### 0.1.x +## 0.x + +#### 0.2.0 + +##### API + +feat: 新增 VioHttpServerOption.frontendConfig 选项,可以直接更改前端配置 +feat: 新增 VioHttpServerOption.rpcAuthenticate 选项,可对RPC连接鉴权 +feat: VioHttpServerOption 废弃 staticHandler,改为 requestHandler + +feat: ChartCreateOption 新增 name、updateThrottle、onRequestUpdate选项 +feat!: 废弃 vio.chart, 请使用 vio.object 代替 +feat!: 新增 `vio.object.createTable()` + +fix: http server 关闭连接应断开所有http连接 + +refactor!: RPC 接口更改 + +BREAKING CHANGE: 删除导出 ChartCenter +BREAKING CHANGE: 删除导出 FileData 请使用 VioFileData 代替 +BREAKING CHANGE: `VioChart.onRequestUpdate()` 改为 `VioChart.requestUpdate()` + +##### WEB + +feat: chart 面板 tab 标签的标题显示未 VioChart.name #### 0.1.1 diff --git a/README.md b/README.md index 5db861c..dc06ad7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Web 终端 -提供各种图形化的控件。在浏览器中与进程进行交互 +提供各种图形化的控件。通过基于 WebSocket 的 RPC ([cpcall](https://github.com/asnowc/cpcall)) 通信,在浏览器中与进程进行交互 @@ -34,8 +34,8 @@ import vio, { VioHttpServer } from "@asla/vio"; ### 权限 -- --allow-net:启动 web 服务器 -- --allow-read:web 服务器读取文件 +- `--allow-net`:启动 web 服务器 +- `--allow-read`:web 服务器读取文件 ```ts import vio, { VioHttpServer } from "jsr:@asla/vio"; @@ -64,5 +64,5 @@ setInterval(() => { 在浏览器访问 https://127.0.0.1:8887,你看到的就是 vio 的 WEB 终端。你可以与其进行交互 -[输出图表的示例](https://github.com/asnowc/vio/blob/main/docs/usage/chart.md) +[输出图表的示例](https://github.com/asnowc/vio/blob/main/docs/usage/chart.md)\ [WEB终端 输出与输入示例](https://github.com/asnowc/vio/blob/main/docs/usage/tty.md) diff --git a/docs/usage/chart.md b/docs/usage/chart.md index ae74dee..58cb5f4 100644 --- a/docs/usage/chart.md +++ b/docs/usage/chart.md @@ -12,7 +12,7 @@ function getMemoryChartData() { return [data.external, data.heapUsed, data.heapTotal, data.rss]; // 应与 indexNames 对应 } -const chart = vio.chart.create(2, { +const chart = vio.object.create(2, { meta: { chartType: "line", //折线图 title: "内存", // 图表标题 @@ -24,7 +24,7 @@ const chart = vio.chart.create(2, { setInterval(() => { chart.updateData(getMemoryChartData()); // 每秒更新一次图的数据 }, 1000); -vio.chart.dispose(chart); // 如果不再使用,应销毁 +vio.object.dispose(chart); // 如果不再使用,应销毁 ``` #### 被动更新 @@ -32,7 +32,7 @@ vio.chart.dispose(chart); // 如果不再使用,应销毁 有时候,我们不想在没有连接的时候去更新图的数据,因为获取图的数据可能是非常耗费性能的,我们可以让客户端决定何时更新图的数据 ```ts -const chart = vio.chart.create(2, { +const chart = vio.object.create(2, { meta: { chartType: "line", title: "内存", @@ -81,7 +81,7 @@ const dimensions = { indexNames: ["external", "heapUsed", "heapTotal", "rss"], }, }; -const chart = vio.chart.create(2, { +const chart = vio.object.create(2, { meta, dimensions: dimensions, //设置 维度信息 }); diff --git a/e2e/test/chart/chart.spec.ts b/e2e/test/chart/chart.spec.ts index 961ff6f..fc92be1 100644 --- a/e2e/test/chart/chart.spec.ts +++ b/e2e/test/chart/chart.spec.ts @@ -8,12 +8,15 @@ beforeEach(async ({ appPage, vioServerInfo: { visitUrl } }) => { }); test("二维折线图更新", async function ({ vioServerInfo: { vio }, appPage: page }) { const maxDiffPixelRatio = 0.02; - const chart = vio.chart.create(2, { meta: { chartType: "line", enableTimeline: true, title: "图测试1" } }); + const chart = vio.object.createChart(2, { + name: "图测试1", + meta: { chartType: "line", enableTimeline: true, title: "图测试1" }, + }); chart.updateData([2, 8]); chart.updateData([12, 3]); await page.getByLabel("dashboard").locator("svg").click(); - await page.getByRole("button", { name: "line-chart 图测试1" }).click(); // 打开面板 + await page.getByRole("button", { name: chart.name }).click(); // 打开面板 const chartPanel = page.locator(`.${E2E_SELECT_CLASS.panels.chart}`); await expect(chartPanel.count(), "面板已打开").resolves.toBe(1); @@ -35,17 +38,23 @@ test("创建与删除图", async function ({ vioServerInfo: { vio }, appPage: pa await page.getByLabel("dashboard").locator("svg").click(); - const chart1 = vio.chart.create(2, { meta: { chartType: "bar", title: "abc图1", requestInterval: 1234 } }); + const chart1 = vio.object.createChart(2, { + name: "abc图1", + meta: { chartType: "bar", title: "abc图1", requestInterval: 1234 }, + }); await page.getByRole("button", { name: chart1.meta.title! }).click(); // 打开面板 await expect(chartPanel.count(), "图1面板已打开").resolves.toBe(1); - const chart2 = vio.chart.create(1, { meta: { chartType: "gauge", title: "abc图2", requestInterval: 8769 } }); - await page.getByRole("button", { name: chart2.meta.title! }).click(); // 打开面板 + const chart2 = vio.object.createChart(1, { + name: "abc图2", + meta: { chartType: "gauge", title: "abc图2", requestInterval: 8769 }, + }); + await page.getByRole("button", { name: chart2.name! }).click(); // 打开面板 await page.getByRole("button", { name: "setting" }).click(); //打开设置面板 await expect(page.getByLabel("自动请求更新间隔")).toHaveValue(chart2.meta.requestInterval!.toString()); //通过 requestInterval 判读面板属于 chart2 - vio.chart.disposeChart(chart1); + vio.object.disposeObject(chart1); await afterTime(100); await expect(page.getByRole("button", { name: chart1.meta.title! }).count(), "侧栏统计已被删除").resolves.toBe(0); }); diff --git a/package.json b/package.json index 81da442..ea74fa0 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "ci:build": "pnpm run -r ci:build", "ci:test": "vitest run", "ci:check-api": "pnpm run -r ci:api-check", + "type-check": "pnpm run -r type-check", "test:coverage": "vitest --coverage --ui" }, "keywords": [], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6be9bc3..9042288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,10 @@ importers: version: 0.0.3(tslib@2.6.3)(typescript@5.5.3) '@microsoft/api-extractor': specifier: ^7.47.0 - version: 7.47.0(@types/node@20.14.10) + version: 7.47.0(@types/node@22.0.2) '@vitest/coverage-v8': specifier: ^1.6.0 - version: 1.6.0(vitest@1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0)) + version: 1.6.0(vitest@1.6.0(@types/node@22.0.2)(@vitest/ui@1.6.0)) '@vitest/ui': specifier: ^1.6.0 version: 1.6.0(vitest@1.6.0) @@ -34,7 +34,7 @@ importers: version: 5.5.3 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0) + version: 1.6.0(@types/node@22.0.2)(@vitest/ui@1.6.0) e2e: devDependencies: @@ -48,15 +48,15 @@ importers: vio: dependencies: cpcall: - specifier: ^0.6.0 - version: 0.6.0 + specifier: ^0.6.2 + version: 0.6.2 devDependencies: '@eavid/lib-node': specifier: ^2.1.2 version: 2.1.2 '@types/node': - specifier: ^20.14.10 - version: 20.14.10 + specifier: ^22.0.2 + version: 22.0.2 evlib: specifier: ^2.6.1 version: 2.6.1 @@ -76,8 +76,8 @@ importers: specifier: ^5.19.1 version: 5.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) cpcall: - specifier: ^0.6.0 - version: 0.6.0 + specifier: ^0.6.2 + version: 0.6.2 dockview: specifier: ^1.15.0 version: 1.15.0 @@ -100,9 +100,12 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.3.3(@types/node@22.0.2)) vite: specifier: ^5.3.3 - version: 5.3.3(@types/node@20.14.10) + version: 5.3.3(@types/node@22.0.2) packages: @@ -138,6 +141,40 @@ packages: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.25.2': + resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.25.2': + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.0': + resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.2': + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.25.2': + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.24.8': + resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-simple-access@7.24.7': + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.24.8': resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} @@ -146,6 +183,14 @@ packages: resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.24.8': + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.25.0': + resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} + engines: {node: '>=6.9.0'} + '@babel/highlight@7.24.7': resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} @@ -155,14 +200,43 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.25.0': + resolution: {integrity: sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.24.7': + resolution: {integrity: sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.24.7': + resolution: {integrity: sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.24.8': resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} engines: {node: '>=6.9.0'} + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.2': + resolution: {integrity: sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.24.8': resolution: {integrity: sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==} engines: {node: '>=6.9.0'} + '@babel/types@7.25.2': + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -597,11 +671,23 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/node@20.14.10': - resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==} + '@types/node@22.0.2': + resolution: {integrity: sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -615,6 +701,12 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@vitejs/plugin-react@4.3.1': + resolution: {integrity: sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + '@vitest/coverage-v8@1.6.0': resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} peerDependencies: @@ -723,6 +815,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.23.2: + resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -731,6 +828,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caniuse-lite@1.0.30001644: + resolution: {integrity: sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw==} + chai@4.4.1: resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} engines: {node: '>=4'} @@ -767,11 +867,14 @@ packages: confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} - cpcall@0.6.0: - resolution: {integrity: sha512-jes5iAuguRykFcgOKdRKcq8tuioCRGOo9LLFJ47m2UxdZuvLyqOmoD/rkGm9TMdvKVNYGAQB1UPvOvQunDdpfA==} + cpcall@0.6.2: + resolution: {integrity: sha512-9GJjQRJPxxRaBg8h13kSu9fvF1zZqs5OBD+UaZDBESTR18y2Ug2k8YbQKFznf1j2+IPNzsdXbF5YPTFBRvxhmg==} engines: {node: '>=18'} cross-spawn@7.0.3: @@ -823,6 +926,9 @@ packages: echarts@5.5.0: resolution: {integrity: sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==} + electron-to-chromium@1.5.3: + resolution: {integrity: sha512-QNdYSS5i8D9axWp/6XIezRObRHqaav/ur9z1VzCDUCH1XIFOr9WQk5xmgunhsTpjjgDy3oLxO/WMOVZlpUQrlA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -834,6 +940,10 @@ packages: engines: {node: '>=12'} hasBin: true + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -896,6 +1006,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -915,6 +1029,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1013,12 +1131,22 @@ packages: js-tokens@9.0.0: resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json2mq@0.2.0: resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1039,6 +1167,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1046,6 +1177,9 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magicast@0.3.4: resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} @@ -1097,6 +1231,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1416,6 +1553,10 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -1459,6 +1600,10 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} @@ -1615,13 +1760,19 @@ packages: ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.11.1: + resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1704,6 +1855,9 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -1762,34 +1916,140 @@ snapshots: dependencies: '@babel/highlight': 7.24.7 picocolors: 1.0.1 - optional: true + + '@babel/compat-data@7.25.2': {} + + '@babel/core@7.25.2': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.0 + '@babel/parser': 7.25.0 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.2 + '@babel/types': 7.25.2 + convert-source-map: 2.0.0 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.25.0': + dependencies: + '@babel/types': 7.25.2 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-compilation-targets@7.25.2': + dependencies: + '@babel/compat-data': 7.25.2 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.25.2 + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.24.8': {} + + '@babel/helper-simple-access@7.24.7': + dependencies: + '@babel/traverse': 7.25.2 + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color '@babel/helper-string-parser@7.24.8': {} '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-option@7.24.8': {} + + '@babel/helpers@7.25.0': + dependencies: + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + '@babel/highlight@7.24.7': dependencies: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.0.1 - optional: true '@babel/parser@7.24.8': dependencies: '@babel/types': 7.24.8 + '@babel/parser@7.25.0': + dependencies: + '@babel/types': 7.25.2 + + '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/runtime@7.24.8': dependencies: regenerator-runtime: 0.14.1 + '@babel/template@7.25.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.0 + '@babel/types': 7.25.2 + + '@babel/traverse@7.25.2': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/parser': 7.25.0 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.24.8': dependencies: '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.25.2': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + '@bcoe/v8-coverage@0.2.3': {} '@ctrl/tinycolor@3.6.1': {} @@ -1915,23 +2175,23 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@microsoft/api-extractor-model@7.29.2(@types/node@20.14.10)': + '@microsoft/api-extractor-model@7.29.2(@types/node@22.0.2)': dependencies: '@microsoft/tsdoc': 0.15.0 '@microsoft/tsdoc-config': 0.17.0 - '@rushstack/node-core-library': 5.4.1(@types/node@20.14.10) + '@rushstack/node-core-library': 5.4.1(@types/node@22.0.2) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.47.0(@types/node@20.14.10)': + '@microsoft/api-extractor@7.47.0(@types/node@22.0.2)': dependencies: - '@microsoft/api-extractor-model': 7.29.2(@types/node@20.14.10) + '@microsoft/api-extractor-model': 7.29.2(@types/node@22.0.2) '@microsoft/tsdoc': 0.15.0 '@microsoft/tsdoc-config': 0.17.0 - '@rushstack/node-core-library': 5.4.1(@types/node@20.14.10) + '@rushstack/node-core-library': 5.4.1(@types/node@22.0.2) '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.13.0(@types/node@20.14.10) - '@rushstack/ts-command-line': 4.22.0(@types/node@20.14.10) + '@rushstack/terminal': 0.13.0(@types/node@22.0.2) + '@rushstack/ts-command-line': 4.22.0(@types/node@22.0.2) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -2116,7 +2376,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.18.1': optional: true - '@rushstack/node-core-library@5.4.1(@types/node@20.14.10)': + '@rushstack/node-core-library@5.4.1(@types/node@22.0.2)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -2127,23 +2387,23 @@ snapshots: resolve: 1.22.8 semver: 7.5.4 optionalDependencies: - '@types/node': 20.14.10 + '@types/node': 22.0.2 '@rushstack/rig-package@0.5.2': dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.13.0(@types/node@20.14.10)': + '@rushstack/terminal@0.13.0(@types/node@22.0.2)': dependencies: - '@rushstack/node-core-library': 5.4.1(@types/node@20.14.10) + '@rushstack/node-core-library': 5.4.1(@types/node@22.0.2) supports-color: 8.1.1 optionalDependencies: - '@types/node': 20.14.10 + '@types/node': 22.0.2 - '@rushstack/ts-command-line@4.22.0(@types/node@20.14.10)': + '@rushstack/ts-command-line@4.22.0(@types/node@22.0.2)': dependencies: - '@rushstack/terminal': 0.13.0(@types/node@20.14.10) + '@rushstack/terminal': 0.13.0(@types/node@22.0.2) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -2154,11 +2414,32 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.24.8 + '@babel/types': 7.24.8 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.24.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.24.8 + '@babel/types': 7.24.8 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.24.8 + '@types/estree@1.0.5': {} - '@types/node@20.14.10': + '@types/node@22.0.2': dependencies: - undici-types: 5.26.5 + undici-types: 6.11.1 '@types/prop-types@15.7.12': {} @@ -2173,7 +2454,18 @@ snapshots: '@types/resolve@1.20.2': {} - '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0))': + '@vitejs/plugin-react@4.3.1(vite@5.3.3(@types/node@22.0.2))': + dependencies: + '@babel/core': 7.25.2 + '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.3.3(@types/node@22.0.2) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.0.2)(@vitest/ui@1.6.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -2188,7 +2480,7 @@ snapshots: std-env: 3.7.0 strip-literal: 2.1.0 test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0) + vitest: 1.6.0(@types/node@22.0.2)(@vitest/ui@1.6.0) transitivePeerDependencies: - supports-color @@ -2223,7 +2515,7 @@ snapshots: pathe: 1.1.2 picocolors: 1.0.1 sirv: 2.0.4 - vitest: 1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0) + vitest: 1.6.0(@types/node@22.0.2)(@vitest/ui@1.6.0) '@vitest/utils@1.6.0': dependencies: @@ -2267,7 +2559,6 @@ snapshots: ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 - optional: true ansi-styles@4.3.0: dependencies: @@ -2357,10 +2648,19 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.23.2: + dependencies: + caniuse-lite: 1.0.30001644 + electron-to-chromium: 1.5.3 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.2) + builtin-modules@3.3.0: {} cac@6.7.14: {} + caniuse-lite@1.0.30001644: {} + chai@4.4.1: dependencies: assertion-error: 1.1.0 @@ -2376,7 +2676,6 @@ snapshots: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - optional: true check-error@1.0.3: dependencies: @@ -2387,14 +2686,12 @@ snapshots: color-convert@1.9.3: dependencies: color-name: 1.1.3 - optional: true color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: - optional: true + color-name@1.1.3: {} color-name@1.1.4: {} @@ -2404,11 +2701,13 @@ snapshots: confbox@0.1.7: {} + convert-source-map@2.0.0: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 - cpcall@0.6.0: + cpcall@0.6.2: dependencies: jbod: 0.5.0 @@ -2452,6 +2751,8 @@ snapshots: tslib: 2.3.0 zrender: 5.5.0 + electron-to-chromium@1.5.3: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -2482,8 +2783,9 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - escape-string-regexp@1.0.5: - optional: true + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} estree-walker@2.0.2: {} @@ -2548,6 +2850,8 @@ snapshots: function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-func-name@2.0.2: {} get-stream@8.0.1: {} @@ -2574,10 +2878,11 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + globals@11.12.0: {} + graceful-fs@4.2.11: {} - has-flag@3.0.0: - optional: true + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -2657,12 +2962,16 @@ snapshots: js-tokens@9.0.0: {} + jsesc@2.5.2: {} + json-schema-traverse@1.0.0: {} json2mq@0.2.0: dependencies: string-convert: 0.2.1 + json5@2.2.3: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -2684,6 +2993,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -2692,6 +3005,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.3.4: dependencies: '@babel/parser': 7.24.8 @@ -2740,6 +3057,8 @@ snapshots: nanoid@3.3.7: {} + node-releases@2.0.18: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -3138,6 +3457,8 @@ snapshots: react-is@18.3.1: {} + react-refresh@0.14.2: {} + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -3158,7 +3479,7 @@ snapshots: rollup-plugin-dts@6.1.1(rollup@4.18.1)(typescript@5.5.3): dependencies: - magic-string: 0.30.10 + magic-string: 0.30.11 rollup: 4.18.1 typescript: 5.5.3 optionalDependencies: @@ -3198,6 +3519,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.0 + semver@6.3.1: {} + semver@7.5.4: dependencies: lru-cache: 6.0.0 @@ -3267,7 +3590,6 @@ snapshots: supports-color@5.5.0: dependencies: has-flag: 3.0.0 - optional: true supports-color@7.2.0: dependencies: @@ -3315,21 +3637,27 @@ snapshots: ufo@1.5.3: {} - undici-types@5.26.5: {} + undici-types@6.11.1: {} universalify@0.1.2: {} + update-browserslist-db@1.1.0(browserslist@4.23.2): + dependencies: + browserslist: 4.23.2 + escalade: 3.1.2 + picocolors: 1.0.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite-node@1.6.0(@types/node@20.14.10): + vite-node@1.6.0(@types/node@22.0.2): dependencies: cac: 6.7.14 debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.3.3(@types/node@20.14.10) + vite: 5.3.3(@types/node@22.0.2) transitivePeerDependencies: - '@types/node' - less @@ -3340,16 +3668,16 @@ snapshots: - supports-color - terser - vite@5.3.3(@types/node@20.14.10): + vite@5.3.3(@types/node@22.0.2): dependencies: esbuild: 0.21.5 postcss: 8.4.39 rollup: 4.18.1 optionalDependencies: - '@types/node': 20.14.10 + '@types/node': 22.0.2 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.14.10)(@vitest/ui@1.6.0): + vitest@1.6.0(@types/node@22.0.2)(@vitest/ui@1.6.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -3368,11 +3696,11 @@ snapshots: strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.3.3(@types/node@20.14.10) - vite-node: 1.6.0(@types/node@20.14.10) + vite: 5.3.3(@types/node@22.0.2) + vite-node: 1.6.0(@types/node@22.0.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.14.10 + '@types/node': 22.0.2 '@vitest/ui': 1.6.0(vitest@1.6.0) transitivePeerDependencies: - less @@ -3406,6 +3734,8 @@ snapshots: wrappy@1.0.2: {} + yallist@3.1.1: {} + yallist@4.0.0: {} yocto-queue@1.1.1: {} diff --git a/vio-web/package.json b/vio-web/package.json index b2d0545..14c06de 100644 --- a/vio-web/package.json +++ b/vio-web/package.json @@ -6,18 +6,19 @@ "build": "vite build", "ci:build": "vite build", "dev": "vite", - "type:check": "tsc -p src/tsconfig.json --skipLibCheck" + "type-check": "tsc -p src/tsconfig.json --skipLibCheck" }, "devDependencies": { "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", "vite": "^5.3.3" }, "dependencies": { "@ant-design/icons": "^5.3.7", "@asla/vio": "workspace:^", "antd": "^5.19.1", - "cpcall": "^0.6.0", + "cpcall": "^0.6.2", "dockview": "^1.15.0", "echarts-comp": "^0.4.0", "evlib": "^2.6.1", diff --git a/vio-web/src/hooks/event.ts b/vio-web/src/hooks/event.ts index 2cdaa8b..0fdc660 100644 --- a/vio-web/src/hooks/event.ts +++ b/vio-web/src/hooks/event.ts @@ -2,10 +2,11 @@ import { Listenable } from "evlib"; import { useCallback, useLayoutEffect, useRef, useState } from "react"; /** evlib 监听 Listenable,组件卸载后自动取消监听 */ -export function useListenable(event: Listenable, listener: (data: T) => void) { +export function useListenable(event: Listenable | undefined, listener: (data: T) => void) { const listenerRef = useRef(listener); listenerRef.current = listener; useLayoutEffect(() => { + if (!event) return; const listener = event.on((data) => { listenerRef.current(data); }); diff --git a/vio-web/src/services/ViewApi.ts b/vio-web/src/services/ViewApi.ts index 46eb59e..1b2d40b 100644 --- a/vio-web/src/services/ViewApi.ts +++ b/vio-web/src/services/ViewApi.ts @@ -8,6 +8,7 @@ export class ViewApi { this.#viewApi = viewApi; this._init(); } + #viewApi: DockviewApi; private _init() { this.#viewApi.onDidRemovePanel((e) => { const panelId = e.id; @@ -16,19 +17,19 @@ export class ViewApi { if (ttyId !== null) { this.openedTtyIds = this.openedTtyIds.filter((id) => id !== ttyId); this.openedTtyIdsChange.emit(); - } else if (panelId.startsWith(CHART_PANEL_ID_PREFIX)) { - this.#openedChartPanel.delete(panelId); + } else if (panelId.startsWith(OBJECT_PANEL_ID_PREFIX)) { + this.#openedObjPanel.delete(panelId); } }); this.#viewApi.onDidLayoutFromJSON(() => { - this.#openedChartPanel.clear(); + this.#openedObjPanel.clear(); let openedTtyIds: number[] = []; //布局变化,重新计算状态 for (const panel of this.#viewApi.panels) { const id = panel.id; const ttyId = parseTtyId(id); if (ttyId !== null) openedTtyIds.push(ttyId); - else if (id.startsWith("chart-")) this.#openedChartPanel.set(id, panel); + else if (id.startsWith(OBJECT_PANEL_ID_PREFIX)) this.#openedObjPanel.set(id, panel); } this.openedTtyIds = openedTtyIds; this.openedTtyIdsChange.emit(); @@ -48,7 +49,16 @@ export class ViewApi { } return true; } - #viewApi: DockviewApi; + /* Bar */ + readonly functionOpenedChange = new EventTrigger(); + functionOpened?: string; + openFunctionBar(key?: string) { + this.functionOpened = key; + this.functionOpenedChange.emit(key); + } + + /* TTY */ + openedTtyIds: number[] = []; readonly openedTtyIdsChange = new EventTrigger(); openTtyPanel(index: number) { @@ -68,32 +78,51 @@ export class ViewApi { getOpenedTtyPanel(ttyId: number) { return this.#viewApi.getPanel(genTtyPanelId(ttyId)); } - #openedChartPanel = new Map(); + + /* Chart */ + + #openedObjPanel = new Map(); getOpenedChartPanel(chartId: number) { - return this.#openedChartPanel.get(CHART_PANEL_ID_PREFIX + chartId); + return this.#openedObjPanel.get(OBJECT_PANEL_ID_PREFIX + chartId); } - openChartPanel(chartId: number) { + openChartPanel(chartId: number, title = chartId.toString()) { if (typeof chartId !== "number") throw new ParameterTypeError(0, "number", typeof chartId, "index"); - const chartPanelId = CHART_PANEL_ID_PREFIX + chartId; + const chartPanelId = OBJECT_PANEL_ID_PREFIX + chartId; let panel = this.#viewApi.getPanel(chartPanelId); if (panel) return panel.focus(); - const firstPanel: IDockviewPanel | undefined = this.#openedChartPanel.values().next().value; + const firstPanel: IDockviewPanel | undefined = this.#openedObjPanel.values().next().value; panel = this.#viewApi.addPanel({ id: chartPanelId, component: panels.VioChart, - title: "Chart " + chartId, + title, params: { chartId, TabIcon: "DashboardOutlined" }, position: firstPanel ? { referencePanel: firstPanel } : undefined, }); - this.#openedChartPanel.set(chartPanelId, panel); + this.#openedObjPanel.set(chartPanelId, panel); } - readonly functionOpenedChange = new EventTrigger(); - functionOpened?: string; - openFunctionBar(key?: string) { - this.functionOpened = key; - this.functionOpenedChange.emit(key); + + getOpenedTablePanel(chartId: number) { + return this.#openedObjPanel.get(OBJECT_PANEL_ID_PREFIX + chartId); + } + + openTablePanel(tableId: number, title = tableId.toString()) { + if (typeof tableId !== "number") throw new ParameterTypeError(0, "number", typeof tableId, "index"); + const chartPanelId = OBJECT_PANEL_ID_PREFIX + tableId; + let panel = this.#viewApi.getPanel(chartPanelId); + if (panel) return panel.focus(); + + const firstPanel: IDockviewPanel | undefined = this.#openedObjPanel.values().next().value; + + panel = this.#viewApi.addPanel({ + id: chartPanelId, + component: panels.VioTable, + title, + params: { objectId: tableId, TabIcon: "TableOutlined" }, + position: firstPanel ? { referencePanel: firstPanel } : undefined, + }); + this.#openedObjPanel.set(chartPanelId, panel); } } @@ -125,7 +154,7 @@ type LayoutInfo = { version: number; data: object; }; -const CHART_PANEL_ID_PREFIX = "chart-"; +const OBJECT_PANEL_ID_PREFIX = "object-"; const TTY_PANEL_ID_PREFIX = "tty-"; function genTtyPanelId(index: number) { return TTY_PANEL_ID_PREFIX + index; diff --git a/vio-web/src/services/VioApi.ts b/vio-web/src/services/VioApi.ts index b0727e1..f3e427d 100644 --- a/vio-web/src/services/VioApi.ts +++ b/vio-web/src/services/VioApi.ts @@ -1,23 +1,15 @@ -import { - ChartCreateInfo, - ChartUpdateData, - TtyInputsReq, - TtyOutputsData, - VioClientExposed, - VioServerExposed, -} from "@asla/vio/client"; +import { VioClientExposed, VioServerExposed } from "@asla/vio/client"; import { CpCall, MakeCallers, createWebSocketCpc } from "cpcall"; import { connectWebsocket, WsConnectConfig } from "../lib/websocket.ts"; import React, { createContext } from "react"; import { TtyViewService } from "./vio_api/TtyViewService.ts"; -import { ChartsDataCenterService } from "./vio_api/ChartsDataCenterService.ts"; import { LogService } from "./vio_api/LogService.ts"; import { EventTrigger } from "evlib"; +import { ClientVioObjectService } from "./vio_api/ClientVioObjectService.ts"; +export * from "./vio_api/ClientVioObjectService.ts"; export * from "./vio_api/TtyViewService.ts"; -export * from "./vio_api/ChartsDataCenterService.ts"; export * from "./vio_api/LogService.ts"; export type { WsConnectConfig }; -type MaybePromise = T | Promise; export enum RpcConnectStatus { disconnected = 0, @@ -57,36 +49,12 @@ export class VioRpcApi { this.#cpc?.dispose(); this.#cpc = null; } + readonly chart = new ClientVioObjectService(); + readonly tty = new TtyViewService(); #clientRoot: VioClientExposed = { - createChart: (chartInfo: ChartCreateInfo): void => { - this.chart.createChart(chartInfo); - this.log.pushLog("chart", chartInfo, "create"); - }, - deleteChart: (chartId: number): void => { - this.chart.deleteChart(chartId); - this.log.pushLog("chart", chartId, "delete"); - }, - writeChart: (chartId: number, data: Readonly>): void => { - this.chart.writeChart(chartId, data); - this.log.pushLog("chart", data, "update"); - }, - sendTtyReadRequest: (id: number, requestId: number, opts: TtyInputsReq): MaybePromise => { - this.log.pushLog("tty", { id, data: opts }, "input"); - return this.tty.get(id, true).addReading(requestId, opts); - }, - - writeTty: (id: number, data: TtyOutputsData): void => { - this.tty.get(id, true).addOutput(data); - this.log.pushLog("tty", { id, data }, "output"); - }, - ttyReadEnableChange: (ttyId, enable) => { - const tty = this.tty.get(ttyId, true); - tty.setReadEnable(enable, { passive: true }); - }, + object: this.chart, + tty: this.tty, }; - // readonly tty = new TtyViewService(); // 纯输出 - readonly chart = new ChartsDataCenterService(); // 纯输出 - readonly tty = new TtyViewService(); readonly log = new LogService(); @@ -94,18 +62,15 @@ export class VioRpcApi { #serverApi?: MakeCallers; async loadTtyCache(id: number) { if (!this.#serverApi) return []; - return this.#serverApi.getTtyCache(id); + return this.#serverApi.tty.getTtyCache(id); } private onCpcConnect(cpc: CpCall) { - this.chart.clearChart(); + this.chart.clearObject(); this.#cpc = cpc; cpc.setObject(this.#clientRoot satisfies VioClientExposed); this.#serverApi = cpc.genCaller(); - this.#serverApi.getCharts().then(({ list }) => { - this.chart.setCache(list); - }); - this.tty.init(this.#serverApi!); - this.chart.init(this.#serverApi); + this.tty.init(this.#serverApi.tty); + this.chart.init(this.#serverApi.object); cpc.onClose .finally(() => { this.status = RpcConnectStatus.disconnected; diff --git a/vio-web/src/services/vio_api/ChartsDataCenterService.ts b/vio-web/src/services/vio_api/ChartsDataCenterService.ts deleted file mode 100644 index 96a56e9..0000000 --- a/vio-web/src/services/vio_api/ChartsDataCenterService.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - ChartUpdateData, - ChartCreateInfo, - VioChartImpl, - ChartController, - ChartInfo, - VioChartCreateConfig, - VioServerExposed, - ChartDataItem, -} from "@asla/vio/client"; -import { EventTrigger } from "evlib"; -import { MakeCallers } from "cpcall"; - -export class ChartsDataCenterService implements ChartController { - constructor() {} - #chartsMap = new Map>(); - /** @deprecated */ - readonly chartsMap: Map> = this.#chartsMap; - get(id: number) { - return this.#chartsMap.get(id); - } - getAll() { - return this.#chartsMap.values(); - } - get size() { - return this.#chartsMap.size; - } - createChart(crateInfo: ChartCreateInfo): void { - this.#chartsMap.set(crateInfo.id, new ChartClientAgent(crateInfo)); - this.createEvent.emit(crateInfo); - } - clearChart() { - this.#chartsMap.clear(); - this.deleteEvent.emit(undefined); - } - setCache(chartsList: ChartInfo[]) { - for (const item of chartsList) { - const { cacheList = [], ...reset } = item; - const oldChart = this.get(item.id); - const chart = new ChartClientAgent({ ...reset, maxCacheSize: 500 }); - chart.pushCache(...cacheList); - - this.#chartsMap.set(item.id, chart); - } - this.createEvent.emit(undefined); - } - deleteChart(chartId: number): void { - this.#chartsMap.delete(chartId); - this.deleteEvent.emit(chartId); - } - writeChart(chartId: number, data: ChartUpdateData): void { - const instance = this.#chartsMap.get(chartId); - if (!instance) { - console.warn("Write unknown chart id:" + chartId); - return; - } else { - instance.pushCache(data); - //todo 更新其他数据 - } - - this.writeEvent.emit({ id: chartId, data }); - } - async requestUpdate(chartId: number): Promise { - if (!this.#serverApi) throw new Error("没有连接"); - const chart = this.#chartsMap.get(chartId); - if (!chart) throw new Error("图不存在"); - const res = await this.#serverApi.requestUpdateChart(chartId); - if (!chart.lastDataItem || res.timestamp > chart.lastDataItem.timestamp) chart.pushCache(res); - } - #serverApi?: MakeCallers>; - init(serverApi?: MakeCallers>) { - this.#serverApi = serverApi; - } - /** 如果参数位undefined, 则是同时创建多个 */ - readonly createEvent = new EventTrigger(); - /** 如果参数位undefined, 则是同时删除多个 */ - readonly deleteEvent = new EventTrigger(); - readonly writeEvent = new EventTrigger<{ id: number; data: ChartUpdateData }>(); -} - -export class ChartClientAgent extends VioChartImpl { - constructor(config: VioChartCreateConfig) { - super(config); - } - pushCache(...items: ChartDataItem[]): void { - super.pushCache(...items); - this.changeEvent.emit(); - } - readonly changeEvent = new EventTrigger(); - updateData(data: T, timeName?: string | undefined): void { - this.pushCache({ data: data, timestamp: Date.now(), timeName: timeName }); - } -} diff --git a/vio-web/src/services/vio_api/ClientVioObjectService.ts b/vio-web/src/services/vio_api/ClientVioObjectService.ts new file mode 100644 index 0000000..23f328e --- /dev/null +++ b/vio-web/src/services/vio_api/ClientVioObjectService.ts @@ -0,0 +1,180 @@ +import { + ChartUpdateData, + VioChartBase, + ServerObjectExposed, + VioObjectCreateDto, + ClientObjectExposed, + Column, + TableCreateOption, + ServerTableExposed, + Key, + TableChanges, +} from "@asla/vio/client"; +import { EventTrigger } from "evlib"; +import { MakeCallers, RpcService, RpcExposed } from "cpcall"; +import { ChartDataItem, ChartInfo, TableFilter, TableRow, VioChartCreateConfig, VioObject } from "@asla/vio/client"; + +@RpcService() +export class ClientVioObjectService implements ClientObjectExposed { + protected uiObjects = new Map(); + @RpcExposed() + deleteObject(...idList: number[]): void { + let deleted: Record = {}; + for (const id of idList) { + let obj = this.uiObjects.get(id); + if (!obj) { + console.error("RPC 删除不存在的 VioObject", id); + continue; + } + deleted[obj.type] = true; + this.uiObjects.delete(id); + } + this.deleteObjEvent.emit(new Set(idList)); + } + @RpcExposed() + createObject(info: VioObjectCreateDto): void { + this.uiObjects.set(info.id, info); + this.createObjEvent.emit(info); + } + readonly createObjEvent = new EventTrigger(); + readonly deleteObjEvent = new EventTrigger>(); + private async unwatchObject(objectId: number) {} + + clearObject() { + const keys = new Set(this.uiObjects.keys()); + this.uiObjects.clear(); + this.#watchingChart = {}; + this.#watchingTable = {}; + + this.deleteObjEvent.emit(keys); + } + + /* Chart */ + + @RpcExposed() + writeChart(chartId: number, data: ChartUpdateData): void { + this.writeChartEvent.emit({ id: chartId, data }); + } + + #serverApi?: MakeCallers; + init(serverApi?: MakeCallers) { + this.#serverApi = serverApi; + if (serverApi) { + this.uiObjects.clear(); + serverApi.getObjects().then(({ list }) => { + for (const item of list) { + if (this.uiObjects.has(item.id)) continue; + this.uiObjects.set(item.id, item); + } + if (list?.length) { + this.createObjEvent.emit(undefined); + } + }); + } + } + #watchingChart: Record> = {}; + async getChart(chartId: number): Promise | undefined> { + return this.#serverApi!.getChartInfo(chartId); + } + async unwatchChart(chartId: number) { + delete this.#watchingTable[chartId]; + await this.unwatchObject(chartId); + } + async requestUpdate(chartId: number): Promise | undefined> { + if (!this.#serverApi) throw new Error("没有连接"); + return this.#serverApi.requestUpdateChart(chartId); + } + *getChartSampleList() { + for (const item of this.uiObjects.values()) { + if (item.type === "chart") yield item; + } + } + readonly writeChartEvent = new EventTrigger<{ id: number; data: ChartUpdateData }>(); + + /* Table */ + + #watchingTable: Record> = {}; + async getTable(tableId: number): Promise { + if (this.#watchingTable[tableId]) return this.#watchingTable[tableId]; + + const promise = this.#serverApi!.getTable(tableId).then( + async ({ columns, id, ...option }) => { + const table = new TableClientAgent(tableId, columns, option, this.#serverApi!); + this.#watchingTable[id] = table; + return table; + }, + (err) => { + delete this.#watchingTable[tableId]; + throw err; + }, + ); + this.#watchingTable[tableId] = promise; + return promise; + } + async getTableData(tableId: number, filter?: TableFilter) { + return this.#serverApi!.getTableData(tableId, filter); + } + async unwatchTable(tableId: number) { + delete this.#watchingTable[tableId]; + await this.unwatchObject(tableId); + } + #getTable(id: number) { + const table = this.#watchingTable[id]; + if (table instanceof TableClientAgent) return table; + } + @RpcExposed() + updateTable(tableId: number): void { + const table = this.#getTable(tableId); + if (!table) return; + table.needReloadEvent.emit(); + } + @RpcExposed() + tableChange(tableId: number, changes: TableChanges): void { + const table = this.#getTable(tableId); + if (!table) return; + table.needReloadEvent.emit(); + } + + *getTableSampleList() { + for (const item of this.uiObjects.values()) { + if (item.type === "table") yield item; + } + } +} + +export class ChartClientAgent extends VioChartBase { + constructor(config: VioChartCreateConfig) { + super(config); + } + pushCache(...items: ChartDataItem[]): void { + super.pushCache(...items); + } + updateData(data: T, timeName?: string | undefined): void { + this.pushCache({ data: data, timestamp: Date.now(), timeName: timeName }); + } +} +export class TableClientAgent { + constructor( + readonly id: number, + readonly columns: Readonly>[], + option: TableCreateOption, + private api: MakeCallers, + ) { + this.config = option ?? {}; + } + readonly config: TableCreateOption; + readonly needReloadEvent = new EventTrigger(); + async onRowAction(opKey: string, rowKey: Key) { + await this.api.onTableRowAction(this.id, opKey, rowKey); + } + async onTableAction(opKey: string, selectedKeys: Key[]) { + await this.api.onTableAction(this.id, opKey, selectedKeys); + } + async onAdd(param: Add) { + await this.api.onTableRowAdd(this.id, param); + } + async onUpdate(rowKey: Key, param: Update) { + await this.api.onTableRowUpdate(this.id, rowKey, param); + } +} +export type { Key }; diff --git a/vio-web/src/services/vio_api/LogService.ts b/vio-web/src/services/vio_api/LogService.ts index 9ac3ef2..f314ce2 100644 --- a/vio-web/src/services/vio_api/LogService.ts +++ b/vio-web/src/services/vio_api/LogService.ts @@ -27,4 +27,4 @@ export type VioRpcLogInfo = { date: number; id: number; }; -export type VioRpcLogType = "message" | "notice" | "chart" | "tty"; +export type VioRpcLogType = "message" | "notice" | "chart" | "object" | "tty"; diff --git a/vio-web/src/services/vio_api/TtyViewService.ts b/vio-web/src/services/vio_api/TtyViewService.ts index 8095a79..c8e13d4 100644 --- a/vio-web/src/services/vio_api/TtyViewService.ts +++ b/vio-web/src/services/vio_api/TtyViewService.ts @@ -1,8 +1,10 @@ -import { TtyInputsReq, TtyOutputData, TtyOutputsData } from "@asla/vio/client"; +import { ClientTtyExposed, TtyInputsReq, TtyOutputData, TtyOutputsData } from "@asla/vio/client"; +import { RpcExposed, RpcService } from "cpcall"; import { EventTrigger } from "evlib"; import { LinkedCacheQueue, LinkedQueue, LoopUniqueId } from "evlib/data_struct"; -export class TtyViewService { +@RpcService() +export class TtyViewService implements ClientTtyExposed { /** 输出队列消息增加或减少时触发 */ readonly outputChangeEvent = new EventTrigger<{ id: number }>(); /** 输入队列的请求项增加或减少时触发 */ @@ -65,6 +67,20 @@ export class TtyViewService { } resolver?: TtyResolver; #ttys: Record = {}; + + @RpcExposed() + sendTtyReadRequest(ttyId: number, requestId: number, opts: TtyInputsReq): void { + return this.get(ttyId, true).addReading(requestId, opts); + } + @RpcExposed() + writeTty(ttyId: number, data: TtyOutputsData): void { + this.get(ttyId, true).addOutput(data); + } + @RpcExposed() + ttyReadEnableChange(ttyId: number, enable: boolean): void { + const tty = this.get(ttyId, true); + tty.setReadEnable(enable, { passive: true }); + } } export interface TtyResolver { resolveTtyReadRequest(ttyId: number, requestId: number, res: TtyInputsReq): Promise; diff --git a/vio-web/src/services/vio_api/_ClientVioObjectService.ts b/vio-web/src/services/vio_api/_ClientVioObjectService.ts new file mode 100644 index 0000000..1f2dfa9 --- /dev/null +++ b/vio-web/src/services/vio_api/_ClientVioObjectService.ts @@ -0,0 +1,45 @@ +import { ClientObjectBaseExposed, VioObjectCreateDto, VioObject } from "@asla/vio/client"; +import { EventTrigger } from "evlib"; +import { RpcService, RpcExposed } from "cpcall"; + +@RpcService() +export class ClientVioObjectBaseService implements ClientObjectBaseExposed { + protected uiObjects = new Map(); + @RpcExposed() + deleteObject(...idList: number[]): void { + for (const id of idList) { + this.uiObjects.delete(id); + } + this.deleteEvent.emit(undefined); + } + @RpcExposed() + createObject(info: VioObjectCreateDto): void { + this.uiObjects.set(info.id, info); + this.createEvent.emit(info); + } + + protected async unwatchObject(objectId: number) {} + + get(id: number): VioObject | undefined { + return this.uiObjects.get(id); + } + getAll(): IterableIterator { + return this.uiObjects.values(); + } + clearObject() { + this.uiObjects.clear(); + this.deleteEvent.emit(undefined); + } + get size() { + return this.uiObjects.size; + } + + getObjectInfo(id: number): VioObject | undefined { + return this.uiObjects.get(id); + } + + /** 如果参数位undefined, 则是同时删除多个 */ + readonly deleteEvent = new EventTrigger(); + /** 如果参数位undefined, 则是同时创建多个 */ + readonly createEvent = new EventTrigger(); +} diff --git a/vio-web/src/views/components/ConfigurableChart/ConfigurableChart.tsx b/vio-web/src/views/components/ConfigurableChart/ConfigurableChart.tsx index aa277a7..b8faf93 100644 --- a/vio-web/src/views/components/ConfigurableChart/ConfigurableChart.tsx +++ b/vio-web/src/views/components/ConfigurableChart/ConfigurableChart.tsx @@ -22,26 +22,32 @@ function useInternalRequest( const { run } = useAsync(() => { timeRef.current = Date.now(); return req().finally(() => { + timerId.current = undefined; if (!requestUpdate || !internal) { timeRef.current = undefined; return; } let afterTime = internal - (Date.now() - timeRef.current!); if (afterTime < 0) run(); - else setTimeout(run, afterTime); + else { + timerId.current = setTimeout(run, afterTime); + } }); }); const timeRef = useRef(); + const timerId = useRef(); useEffect(() => { if (requestUpdate && timeRef.current === undefined) { run(); } + return () => clearTimeout(timerId.current); }, [requestUpdate, internal, ...deps]); return run; } export interface ConfigurableChartChartProps { + loading?: boolean; chart: ChartClientAgent; chartSize?: object; visible?: boolean; @@ -64,7 +70,6 @@ export function ConfigurableChart(props: ConfigurableChartChartProps) { form.setFieldsValue(values); setBoardConfig(values); }, [chart.meta]); - const { chart: chartApi, connected } = useVioApi(); const [data, updateData] = useReducer(function updateData() { let data: Readonly>[] = new Array(chart.cachedSize); @@ -78,7 +83,12 @@ export function ConfigurableChart(props: ConfigurableChartChartProps) { const reload = useInternalRequest( async () => { if (!connected) return; - return chartApi.requestUpdate(chart.id); + + const res = await chartApi.requestUpdate(chart.id); + if (!res) return; + if (chart.lastDataItem && res.timestamp <= chart.lastDataItem.timestamp) return; + chart.pushCache(res); + updateData(); }, { deps: [], internal: requestInterval, requestUpdate: requestUpdate }, ); @@ -92,7 +102,12 @@ export function ConfigurableChart(props: ConfigurableChartChartProps) { return render; }, [displayChartType]); - useListenable(chart.changeEvent, () => visible && updateData()); + useListenable(chartApi.writeChartEvent, ({ data, id }) => { + if (id !== chart.id) return; + chart.pushCache(data); + updateData(); + }); + useMemo(() => visible && updateData(), [visible]); const config = useMemo((): EChartsPruneOption => { diff --git a/vio-web/src/views/components/ConfigurableChart/chart_view/GaugePie.tsx b/vio-web/src/views/components/ConfigurableChart/chart_view/GaugePie.tsx index 9640514..77a5640 100644 --- a/vio-web/src/views/components/ConfigurableChart/chart_view/GaugePie.tsx +++ b/vio-web/src/views/components/ConfigurableChart/chart_view/GaugePie.tsx @@ -7,8 +7,8 @@ import { } from "@/lib/echarts.ts"; import { ChartCommonProps } from "../type.ts"; import { useContext, useLayoutEffect, useMemo } from "react"; -import { ChartMeta } from "@asla/vio/client"; import { errorCollector } from "@/services/ErrorLog.ts"; +import { ChartMeta } from "@asla/vio/client"; export function GaugePie(props: ChartCommonProps) { const { resizeDep, staticOptions, chartMeta, staticSeries, dataList, dimensions } = props; diff --git a/vio-web/src/views/components/ConfigurableChart/chart_view/XYCoordChart.tsx b/vio-web/src/views/components/ConfigurableChart/chart_view/XYCoordChart.tsx index 2ae2177..d796755 100644 --- a/vio-web/src/views/components/ConfigurableChart/chart_view/XYCoordChart.tsx +++ b/vio-web/src/views/components/ConfigurableChart/chart_view/XYCoordChart.tsx @@ -1,9 +1,8 @@ import { EChartsPruneOption, EChartsPruneSeries, useEChart } from "@/lib/echarts.ts"; import { ChartCommonProps } from "../type.ts"; import { memo, useLayoutEffect, useMemo, useRef } from "react"; -import { ChartDataItem, DimensionInfo } from "@asla/vio/client.ts"; +import { ChartDataItem, DimensionInfo } from "@asla/vio/client"; import { formatTime } from "../util/data_transfrom.ts"; -import React from "react"; const DISPLAY_ONLY: string[] = ["line", "bar", "scatter"]; export const XYCoordChart = memo(function XYCoordChart(props: ChartCommonProps) { diff --git a/vio-web/src/views/components/ConfigurableChart/type.ts b/vio-web/src/views/components/ConfigurableChart/type.ts index d1dacd3..ab06eb5 100644 --- a/vio-web/src/views/components/ConfigurableChart/type.ts +++ b/vio-web/src/views/components/ConfigurableChart/type.ts @@ -15,4 +15,5 @@ export interface ChartConfig extends ChartMeta.Common { echartsOption?: object; echartsSeries?: object; requestUpdate?: boolean; + enableTimeline?: boolean; } diff --git a/vio-web/src/views/components/ConfigurableChart/util/data_transfrom.ts b/vio-web/src/views/components/ConfigurableChart/util/data_transfrom.ts index 69be332..0f6da5a 100644 --- a/vio-web/src/views/components/ConfigurableChart/util/data_transfrom.ts +++ b/vio-web/src/views/components/ConfigurableChart/util/data_transfrom.ts @@ -1,4 +1,4 @@ -import { ChartDataItem } from "@asla/vio/client.ts"; +import { ChartDataItem } from "@asla/vio/client"; function parseDate(timestamp: number) { const date = new Date(timestamp); diff --git a/vio-web/src/views/function_bars/mod.tsx b/vio-web/src/views/function_bars/mod.tsx index b8892b1..f8eecc3 100644 --- a/vio-web/src/views/function_bars/mod.tsx +++ b/vio-web/src/views/function_bars/mod.tsx @@ -1,9 +1,9 @@ import React, { PropsWithChildren } from "react"; -import { BugOutlined, CodeOutlined, DashboardOutlined } from "@ant-design/icons"; +import { BugOutlined, CodeOutlined, DashboardOutlined, TableOutlined } from "@ant-design/icons"; import { Tooltip, Dropdown, MenuProps } from "antd"; import { useViewApi } from "@/services/ViewApi.ts"; import { ConnectControl, LayoutControl } from "./actions/mod.ts"; -import { TtyBar, DebugBar, ChartBar } from "./panels/mod.ts"; +import { TtyBar, DebugBar, ChartBar, TableBar } from "./panels/mod.ts"; import { useThemeToken } from "@/services/AppConfig.ts"; import { useListenableData } from "@/hooks/event.ts"; import { DEV_MODE, E2E_SELECT_CLASS } from "@/const.ts"; @@ -102,6 +102,12 @@ const functionList: BarDefine[] = [ Icon: DashboardOutlined, Content: ChartBar, }, + { + title: "表格", + key: "table", + Icon: TableOutlined, + Content: TableBar, + }, ]; if (import.meta.env.MODE === DEV_MODE) { functionList.push({ diff --git a/vio-web/src/views/function_bars/panels/chart.tsx b/vio-web/src/views/function_bars/panels/chart.tsx index ce811e9..51ea972 100644 --- a/vio-web/src/views/function_bars/panels/chart.tsx +++ b/vio-web/src/views/function_bars/panels/chart.tsx @@ -12,27 +12,24 @@ export function ChartBar() { const viewApi = useViewApi(); const { chart } = useVioApi(); const forceUpdate = useForceUpdate(); - useListenable(chart.createEvent, forceUpdate); - useListenable(chart.deleteEvent, forceUpdate); + useListenable(chart.createObjEvent, forceUpdate); + useListenable(chart.deleteObjEvent, forceUpdate); - const chartList = chart.getAll(); + const chartList = chart.getChartSampleList(); const itemList = mapIterable(chartList, (item) => { - const meta = item.meta; - const type = meta.chartType; + const type = item.type ?? ""; const Icon = CHART_TYPE_RENDER_MAP[type]?.Icon ?? QuestionCircleOutlined; - const title = meta.title ?? item.id; - const panelApi = viewApi.getOpenedChartPanel(item.id); const onClick = () => { if (panelApi) panelApi.focus(); - else viewApi.openChartPanel(item.id); + else viewApi.openChartPanel(item.id, item.name); }; return ( ); @@ -44,6 +41,3 @@ export function ChartBar() { ); } -interface ChartItem { - type: string; -} diff --git a/vio-web/src/views/function_bars/panels/mod.ts b/vio-web/src/views/function_bars/panels/mod.ts index ca40080..3737722 100644 --- a/vio-web/src/views/function_bars/panels/mod.ts +++ b/vio-web/src/views/function_bars/panels/mod.ts @@ -1,3 +1,4 @@ export * from "./chart.tsx"; export * from "./debug.tsx"; export * from "./tty.tsx"; +export * from "./table.tsx" \ No newline at end of file diff --git a/vio-web/src/views/function_bars/panels/table.tsx b/vio-web/src/views/function_bars/panels/table.tsx new file mode 100644 index 0000000..e150b14 --- /dev/null +++ b/vio-web/src/views/function_bars/panels/table.tsx @@ -0,0 +1,41 @@ +import { useListenable } from "@/hooks/event.ts"; +import { mapIterable } from "@/lib/renderLink.ts"; +import { useViewApi } from "@/services/ViewApi.ts"; +import { RpcConnectStatus, useVioApi } from "@/services/VioApi.ts"; +import { Button, Empty } from "antd"; +import React from "react"; +import { QuestionCircleOutlined } from "@ant-design/icons"; +import { useForceUpdate } from "@/hooks/forceUpdate.ts"; + +export function TableBar() { + const viewApi = useViewApi(); + const { chart } = useVioApi(); + const forceUpdate = useForceUpdate(); + useListenable(chart.createObjEvent, forceUpdate); + useListenable(chart.deleteObjEvent, forceUpdate); + + const list = chart.getTableSampleList(); + + const itemList = mapIterable(list, (item) => { + const Icon = QuestionCircleOutlined; + const onClick = () => { + const panelApi = viewApi.getOpenedTablePanel(item.id); + if (panelApi) panelApi.focus(); + else viewApi.openTablePanel(item.id, item.name); + }; + return ( + + ); + }); + + return ( +
+ {itemList.length > 0 ? itemList : } +
+ ); +} diff --git a/vio-web/src/views/panels/__mocks__/MockCharts.ts b/vio-web/src/views/panels/__mocks__/MockCharts.ts index 9ad933b..07d4361 100644 --- a/vio-web/src/views/panels/__mocks__/MockCharts.ts +++ b/vio-web/src/views/panels/__mocks__/MockCharts.ts @@ -1,6 +1,6 @@ import { setInterval } from "evlib"; -import { ChartClientAgent, ChartsDataCenterService, useVioApi } from "@/services/VioApi.ts"; +import { ChartClientAgent, ClientVioObjectService, useVioApi } from "@/services/VioApi.ts"; import { useLayoutEffect, useMemo } from "react"; import { DimensionInfo } from "@asla/vio/client"; @@ -31,11 +31,7 @@ export class MockChart extends ChartClientAgent { dispose() {} } -export function useMockChart( - type: string, - dimensions: Record, - ...args: number[] -) { +export function useMockChart(type: string, dimensions: Record, ...args: number[]) { const chart = useMemo(() => new MockChart(args, type, dimensions), []); useLayoutEffect(() => { chart.start(); @@ -67,7 +63,7 @@ function radomInt(max: number) { export function startMockUpdateData( id: number, - api: ChartsDataCenterService, + api: ClientVioObjectService, dimensionIndexSizes: number[], time: number = 2000, ) { @@ -77,7 +73,7 @@ export function startMockUpdateData( }, time); } export function useStartMockUpdateData( - api: ChartsDataCenterService, + api: ClientVioObjectService, config: { id: number; dimensionIndexSizes?: number[]; diff --git a/vio-web/src/views/panels/chart.tsx b/vio-web/src/views/panels/chart.tsx index c6d0ad6..2d7fcc0 100644 --- a/vio-web/src/views/panels/chart.tsx +++ b/vio-web/src/views/panels/chart.tsx @@ -1,51 +1,68 @@ import { IDockviewPanelProps } from "dockview"; -import React, { useEffect, useReducer } from "react"; +import React, { useEffect, useMemo, useReducer, useState } from "react"; import { ChartClientAgent, useVioApi } from "@/services/VioApi.ts"; import { ConfigurableChart } from "../components/ConfigurableChart/mod.ts"; import { useForceUpdate } from "@/hooks/forceUpdate.ts"; import { useListenable } from "@/hooks/event.ts"; import { ReactErrorBoundary } from "@/components/ErrorHander.tsx"; import { E2E_SELECT_CLASS } from "@/const.ts"; +import { useAsync } from "@/hooks/async.ts"; +import { Empty } from "antd"; export function VioChart({ api, containerApi, params }: IDockviewPanelProps<{ chartId: number }>) { const chartCenter = useVioApi().chart; const { chartId } = params; - function getChart() { - let chart = chartCenter.get(chartId); - if (!chart) chart = new ChartClientAgent({ dimension: 1, id: chartId }); - return chart; - } - const [chartInstance, update] = useReducer(getChart, undefined, getChart); - // const chartInstance = useMockChart( - // "gauge", - // [ - // ["启动", "下载", "校验", "解压", "关闭"], - // ["11", "22", "33"], - // ], - // 2, - // ); + + const { + loading, + run, + res: chartInstance, + } = useAsync(function getChart() { + return chartCenter.getChart(chartId).then((chartData) => { + if (!chartData) return; + const chart = new ChartClientAgent(chartData); + chart.maxCacheSize = 1024 * 1024; + if (chartData.cacheList instanceof Array) { + for (const item of chartData.cacheList) { + chart.pushCache(item); + } + } + + return chart; + }); + }); const forceUpdate = useForceUpdate(); const [resizeDep, updateResizeDep] = useReducer(() => ({}), {}); - useListenable(chartCenter.createEvent, (e) => { - if (e === undefined || e.id === chartId) update(); + useListenable(chartCenter.createObjEvent, (e) => { + if (e === undefined || e.id === chartId) run(); }); useEffect(() => { const p1 = api.onDidDimensionsChange(updateResizeDep).dispose; //刷新 api.isVisible const p2 = api.onDidVisibilityChange(forceUpdate).dispose; - + run(); return () => [p1, p2].forEach((dispose) => dispose()); }, []); + const [noExist, setNoExist] = useState(false); + useEffect(() => { + setTimeout(() => { + if (!loading && !chartInstance) setNoExist(true); + }, 800); + }); return ( - + {chartInstance && ( + + )} + {!loading && noExist && } ); } diff --git a/vio-web/src/views/panels/components/VioTable.tsx b/vio-web/src/views/panels/components/VioTable.tsx new file mode 100644 index 0000000..62b4c39 --- /dev/null +++ b/vio-web/src/views/panels/components/VioTable.tsx @@ -0,0 +1,134 @@ +import { Key } from "@/services/VioApi.ts"; +import type { TableRenderFn, TableFilter, Column, TableRow, UiButton, UiAction } from "@asla/vio/client"; +import { Button, Space, Table, TableProps } from "antd"; +import React, { ReactNode, useMemo, useRef, useState } from "react"; +import { VioUi } from "./ui_object.tsx"; +type AntColumn = NonNullable; + +export function VioTable< + Row extends TableRow = TableRow, + Add extends object = Row, + Update extends object = Add, +>(props: { + columns: Column[]; + data?: Row[]; + total?: number; + loading?: boolean; + + keyField: string; + name?: string; + operations?: UiAction[]; + updateAction?: boolean; + addAction?: UiButton["props"]; + onTableAction?(opKey: string, selectedKeys: Key[]): void; + onRowAction?(opKey: string, rowKey: Key): void; + onChange?(filter: TableFilter): void; + onCreate?(param: Add): void; + onUpdate?(rowKey: string, param: Update): void; +}) { + const { data, total = 0, onRowAction, onTableAction, onChange, onCreate, onUpdate, operations, keyField } = props; + + const [dataFilter, setDataFilter] = useState<{ + pageSize: number; + page: number; + filters?: Record; + sorter?: Record; + }>({ page: 1, pageSize: 10 }); + const columns: AntColumn = useMemo(() => { + return props.columns.map((column): AntColumn[0] => { + let render: TableRenderFn | undefined; + if (column.render) { + try { + render = Function("args", column.render) as any; + } catch (error) { + console.error("处理渲染函数时出现异常", error); + } + } + + return { + dataIndex: column.dataIndex, + title: column.title ?? String(column.dataIndex), + width: column.width, + render: (item, record, index) => { + if (render) { + try { + item = render({ record, index, column }); + } catch (error) { + console.error("渲染函数执行时抛出异常", error); + return ""; + } + const onAction = (key: string) => { + onRowAction?.(key, record[keyField]); + }; + + if (item instanceof Array) { + return ( + + {item.map((item) => ( + + ))} + + ); + } else { + return ; + } + } + return item; + }, + }; + }); + }, [props.columns]); + const [selectedKeys, setSelectedKeys] = useState<(string | number)[]>([]); + + const ope = useMemo((): ReactNode[] | undefined => { + return operations?.map((item) => ( + onTableAction?.(item.key, selectedKeys)} key={item.key} /> + )); + }, [operations, selectedKeys]); + return ( +
+ + {props.addAction && ( + + )} + {ope} + + 2, + showQuickJumper: total / dataFilter.pageSize > 5, + showTitle: true, + current: dataFilter.page, + pageSize: dataFilter.pageSize, + }} + onChange={(pagination, filters, sorter) => { + const filter: typeof dataFilter = { + pageSize: pagination.pageSize ?? 10, + page: pagination.current ?? 1, + filters, + sorter, + }; + setDataFilter(filter); + + const number = filter.pageSize; + const skip = number * (filter.page - 1); + onChange?.({ number, skip }); + }} + >
+
+ ); +} diff --git a/vio-web/src/views/panels/components/tty/InputFile.tsx b/vio-web/src/views/panels/components/tty/InputFile.tsx index 20a791b..410025c 100644 --- a/vio-web/src/views/panels/components/tty/InputFile.tsx +++ b/vio-web/src/views/panels/components/tty/InputFile.tsx @@ -1,9 +1,10 @@ -import { TtyInputReq, VioFileData } from "@asla/vio/client"; +import { TtyInputReq } from "@asla/vio/client"; import { ListItem } from "@/views/components/ListItem.tsx"; import { INPUT_TYPE_INFO } from "./const.tsx"; import React, { useState } from "react"; import { Button, Space, Upload, UploadFile } from "antd"; -import { autoUnit } from "evlib/math"; +import { autoUnitByte } from "evlib/math"; +import { VioFileData } from "@asla/vio/client"; export type InputFileProps = { req: TtyInputReq.File; @@ -39,11 +40,11 @@ export function InputFile(props: InputFileProps) { customRequest={({ file, onError, onSuccess }) => { if (typeof file === "string") return; if (maxSize && file.size > maxSize) { - onError?.(new Error(`文件限制大小:${autoUnit.byte(maxSize)}`)); + onError?.(new Error(`文件限制大小:${autoUnitByte(maxSize)}`)); return; } if (file.size > MAXIMUM_BEARING_SIZE) { - onError?.(new Error(`超过 ${autoUnit.byte(MAXIMUM_BEARING_SIZE)} 的上传最大承载大小`)); + onError?.(new Error(`超过 ${autoUnitByte(MAXIMUM_BEARING_SIZE)} 的上传最大承载大小`)); } onSuccess?.({}); }} diff --git a/vio-web/src/views/panels/components/ui_object.tsx b/vio-web/src/views/panels/components/ui_object.tsx new file mode 100644 index 0000000..1dda83e --- /dev/null +++ b/vio-web/src/views/panels/components/ui_object.tsx @@ -0,0 +1,47 @@ +import { type UiButton, UiTag } from "@asla/vio/client"; +import { Button, Tag, Tooltip } from "antd"; +import React, { FC, ReactNode } from "react"; +const createImage = (src: string) => { + return ; +}; + +const uiAction: Record> = { + button: function UiButton(props: UiButton["props"] & { onAction?(): void }) { + const { icon: iconUrl, text, type, onAction, disable } = props; + let icon: ReactNode | undefined; + if (iconUrl) { + icon = createImage(iconUrl); + } + const btn = ( + + ); + const tooltip = props.tooltip || (type === "text" && text); + if (tooltip) { + return {btn}; + } + return btn; + }, +}; +const uiOutput: Record> = { + tag: function TableTag(props: UiTag["props"]) { + return ( + + {props.text} + + ); + }, +}; +export function VioUi(props: { object: any; onAction?: (key: string) => void }) { + const { object, onAction } = props; + if (typeof object === "object") { + if (uiAction[object.ui]) { + return React.createElement(uiAction[object.ui], { ...object.props, onAction: () => onAction?.(object.key) }); + } else if (uiOutput[object.ui]) { + return React.createElement(uiOutput[object.ui], object.props); + } + return JSON.stringify(object); + } + return object; +} diff --git a/vio-web/src/views/panels/mod.ts b/vio-web/src/views/panels/mod.ts index 060fb9e..b40d13b 100644 --- a/vio-web/src/views/panels/mod.ts +++ b/vio-web/src/views/panels/mod.ts @@ -2,9 +2,11 @@ export * from "./tty.tsx"; export * from "./chart.tsx"; import { VioTty } from "./tty.tsx"; import { VioChart } from "./chart.tsx"; +import { VioTablePanel } from "./table.tsx"; const panels = { VioTty, VioChart, + VioTable: VioTablePanel, }; export default panels; type PanelsName = { [key in keyof typeof panels]: string }; diff --git a/vio-web/src/views/panels/table.tsx b/vio-web/src/views/panels/table.tsx new file mode 100644 index 0000000..a6d79f2 --- /dev/null +++ b/vio-web/src/views/panels/table.tsx @@ -0,0 +1,89 @@ +import { IDockviewPanelProps } from "dockview"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Key, RpcConnectStatus, useVioApi } from "@/services/VioApi.ts"; +import { useListenable } from "@/hooks/event.ts"; +import { ReactErrorBoundary } from "@/components/ErrorHander.tsx"; +import { useAsync } from "@/hooks/async.ts"; +import { Empty } from "antd"; +import { VioTable } from "./components/VioTable.tsx"; +import { TableFilter } from "@asla/vio/client"; + +export function VioTablePanel({ api, containerApi, params }: IDockviewPanelProps<{ objectId: number }>) { + const vioApi = useVioApi(); + const chartCenter = vioApi.chart; + const { objectId } = params; + + const { + loading, + run: loadTable, + res: table, + } = useAsync(function () { + return chartCenter.getTable(objectId).then((table) => { + loadTableData(); + return table; + }); + }); + const { run: loadTableData, res: tableData = { total: 0, rows: [] as any[] } } = useAsync((filter?: TableFilter) => + vioApi.chart.getTableData(objectId, filter), + ); + const visible = api.isVisible; + const changedRef = useRef(false); + + useListenable(chartCenter.createObjEvent, (e) => { + if (!e || e.id !== objectId) return; + if (table) return; + loadTable(); + }); + + useListenable(vioApi.statusChange, (status) => { + if (status === RpcConnectStatus.connected) { + loadTable(); + } + }); + useListenable(table?.needReloadEvent, () => { + if (visible) loadTableData(); + else { + changedRef.current = true; + } + }); + + useMemo(() => { + if (visible && changedRef.current) { + loadTableData(); + changedRef.current = false; + } + }, [visible]); + + const [noExist, setNoExist] = useState(false); + useEffect(() => { + setTimeout(() => { + if (!loading && !table) setNoExist(true); + }, 500); + if (vioApi.connected) loadTable(); + }, []); + + return ( +
+ + {table && ( + table.onRowAction(opKey, rowKey)} + onTableAction={(opKey, selectedKeys: Key[]) => table.onTableAction(opKey, selectedKeys)} + onCreate={(param) => table.onAdd(param)} + onUpdate={(rowKey, param) => table.onUpdate(rowKey, param)} + total={tableData.total} + onChange={loadTableData} + /> + )} + {!table && noExist && } + +
+ ); +} diff --git a/vio-web/src/views/tabs/DefaultTab.tsx b/vio-web/src/views/tabs/DefaultTab.tsx index db75e7e..7444536 100644 --- a/vio-web/src/views/tabs/DefaultTab.tsx +++ b/vio-web/src/views/tabs/DefaultTab.tsx @@ -1,4 +1,4 @@ -import { CloseOutlined, CodeOutlined, DashboardOutlined } from "@ant-design/icons"; +import { CloseOutlined, CodeOutlined, DashboardOutlined, TableOutlined } from "@ant-design/icons"; import { IDockviewPanelHeaderProps } from "dockview"; import React, { CSSProperties, FC } from "react"; export interface TabInfo { @@ -7,6 +7,7 @@ export interface TabInfo { const TAB_ICONS: Record = { CodeOutlined: CodeOutlined, DashboardOutlined: DashboardOutlined, + TableOutlined: TableOutlined, }; export function DefaultTab({ api, containerApi, params: props }: IDockviewPanelHeaderProps) { const { TabIcon } = props; diff --git a/vio-web/tsconfig.json b/vio-web/tsconfig.json index 6d10491..d17975e 100644 --- a/vio-web/tsconfig.json +++ b/vio-web/tsconfig.json @@ -3,8 +3,7 @@ "compilerOptions": { "paths": { "@/*": ["./src/*"], - "@asla/vio/client": ["./node_modules/@asla/vio/src/client.ts"], - "@asla/vio/*": ["./node_modules/@asla/vio/src/*"] + "@asla/vio/client": ["./node_modules/@asla/vio/src/client.ts"] } } } diff --git a/vio-web/vite.config.ts b/vio-web/vite.config.ts index cf735ea..4a41d09 100644 --- a/vio-web/vite.config.ts +++ b/vio-web/vite.config.ts @@ -1,5 +1,7 @@ import type { ProxyOptions, UserConfig, AliasOptions } from "vite"; import { defineProject } from "vitest/config"; +import react from "@vitejs/plugin-react"; + import path from "node:path"; const root = import.meta.dirname; @@ -14,6 +16,7 @@ const config: UserConfig = { resolve: { alias, }, + esbuild: { target: "es2022" }, build: { target: "es2017", minify: true, @@ -32,7 +35,7 @@ const config: UserConfig = { }, outDir: "../vio/assets/web", }, - + plugins: [react()], optimizeDeps: { exclude: ["@asla/vio"], }, diff --git a/vio/api/vio.api.md b/vio/api/vio.api.md index b44cab8..10f9adf 100644 --- a/vio/api/vio.api.md +++ b/vio/api/vio.api.md @@ -5,33 +5,13 @@ ```ts // @public -export type CenterCreateChartOption = ChartCreateOption & { - onRequestUpdate?(): MaybePromise; - updateThrottle?: number; -}; - -// @public (undocumented) -export class ChartCenter { - // Warning: (ae-forgotten-export) The symbol "ChartController" needs to be exported by the entry point index.d.ts - constructor(ctrl: ChartController); - get chartsNumber(): number; - create(dimension: 1, options?: CenterCreateChartOption): VioChart; - create(dimension: 2, options?: CenterCreateChartOption): VioChart; - create(dimension: 3, options?: CenterCreateChartOption): VioChart; - // (undocumented) - create(dimension: number, options?: CenterCreateChartOption): VioChart; - disposeChart(chart: VioChart): void; - get(chartId: number): VioChart | undefined; - getAll(): IterableIterator>; - requestUpdate(chartId: number): MaybePromise>; - static TTY_DEFAULT_CACHE_SIZE: number; -} - -// @public -export type ChartCreateOption = { +export type ChartCreateOption = { + name?: string; meta?: VioChartMeta; dimensions?: Record; maxCacheSize?: number; + updateThrottle?: number; + onRequestUpdate?(): MaybePromise; }; // @public (undocumented) @@ -54,6 +34,8 @@ export interface ChartInfo { id: number; // (undocumented) meta: VioChartMeta; + // (undocumented) + name?: string; } // @public (undocumented) @@ -116,8 +98,11 @@ export type ChartUpdateOption = { }; // @public (undocumented) -export type ClassToInterface = { - [key in keyof T]: T[key]; +export type Column = { + title?: string; + dataIndex?: keyof Row; + width?: number; + render?: string; }; // @public @@ -130,6 +115,14 @@ export default _default; // @public (undocumented) export type DimensionalityReduction = T extends Array ? P : never; +// @public +export interface DimensionInfo { + // (undocumented) + indexNames?: string[]; + name?: string; + unitName?: string; +} + // @public (undocumented) interface Disposable_2 { // (undocumented) @@ -144,14 +137,10 @@ export type EncodedImageData = { }; // @public (undocumented) -export type FileData = { - name: string; - data: Uint8Array; - mime: string; -}; +export type IntersectingDimension = T extends Array ? P | IntersectingDimension

: never; // @public (undocumented) -export type IntersectingDimension = T extends Array ? P | IntersectingDimension

: never; +export type Key = string | number; // @public (undocumented) export type MaybePromise = T | Promise; @@ -180,13 +169,40 @@ export type SelectItem = { // @public (undocumented) export type SelectKey = string | number; +// @public (undocumented) +export type TableCreateOption = { + keyField: string; + name?: string; + operations?: UiAction[]; + updateAction?: boolean; + addAction?: UiButton["props"]; +}; + +// @public (undocumented) +export type TableFilter = { + skip?: number; + number?: number; +}; + +// @public (undocumented) +export type TableRenderFn = (args: { + record: Row; + index: number; + column: Readonly>; +}) => UiBase | UiBase[]; + +// @public (undocumented) +export type TableRow = { + [key: string]: any; +}; + // @public export abstract class TTY { confirm(title: string, content?: string): Promise; pick(title: string, options: SelectItem[]): Promise; // Warning: (ae-forgotten-export) The symbol "TtyInputsReq" needs to be exported by the entry point index.d.ts abstract read(config: TtyInputsReq): Promise; - readFiles(option?: TtyReadFileOption): Promise; + readFiles(option?: TtyReadFileOption): Promise; readText(title: string, max?: number): Promise; readText(max?: number): Promise; select(title: string, options: SelectItem[], config?: { @@ -200,7 +216,7 @@ export abstract class TTY { writeTable(data: any[][], header?: string[]): void; writeText(title: string, option?: TtyWriteTextType | TTyWriteTextOption): void; // (undocumented) - writeUiLink(ui: VioChart): void; + writeUiLink(ui: VioObject): void; } // @public (undocumented) @@ -251,17 +267,66 @@ export type TTyWriteTextOption = { // @public (undocumented) export type TtyWriteTextType = "warn" | "log" | "error" | "info"; +// @public (undocumented) +export interface UiAction extends UiBase { + // (undocumented) + key: string; +} + +// @public (undocumented) +export interface UiBase { + // (undocumented) + ui: string; +} + +// @public (undocumented) +export class UiButton implements UiAction { + constructor(key: string, props?: UiButton["props"]); + // (undocumented) + readonly key: string; + // (undocumented) + props?: { + icon?: string; + text?: string; + type?: string; + tooltip?: string; + disable?: boolean; + }; + // (undocumented) + readonly ui = "button"; +} + +// @public (undocumented) +export interface UiInput extends UiBase { +} + +// @public (undocumented) +export interface UiOutput extends UiBase { +} + +// @public (undocumented) +export type UiTag = UiOutput & { + ui: "tag"; + props: { + text?: string; + icon?: string; + color?: string; + }; +}; + // @public export interface Vio extends TTY { - readonly chart: ChartCenter; + // @deprecated + readonly chart: VioObjectCenter; // Warning: (ae-forgotten-export) The symbol "WebSocket_2" needs to be exported by the entry point index.d.ts joinFormWebsocket(websocket: WebSocket_2, onDispose?: (viewer: Disposable_2) => void): Disposable_2; + readonly object: VioObjectCenter; readonly tty: TtyCenter; viewerNumber: number; } // @public -export interface VioChart { +export interface VioChart extends VioObject { // (undocumented) cachedSize: number; // (undocumented) @@ -272,22 +337,20 @@ export interface VioChart { // (undocumented) getCacheDateItem(): IterableIterator>>; // (undocumented) - readonly id: number; - // (undocumented) maxCacheSize: number; // (undocumented) readonly meta: VioChartMeta; - onRequestUpdate?: () => MaybePromise; + requestUpdate(): MaybePromise>; + // (undocumented) + readonly type: "chart"; updateData(data: T, timeName?: string): void; updateThrottle: number; } // @public -export type VioChartCreateConfig = ChartCreateOption & { +export type VioChartCreateConfig = ChartCreateOption & { id: number; dimension: number; - onRequestUpdate?(): MaybePromise; - updateThrottle?: number; }; // @public (undocumented) @@ -298,6 +361,13 @@ export type VioChartMeta = ChartMeta.Progress | ChartMeta.Gauge | ChartMeta.Line // @public (undocumented) export type VioChartType = VioChartMeta["chartType"]; +// @public (undocumented) +export type VioFileData = { + name: string; + data: Uint8Array; + mime: string; +}; + // @public export class VioHttpServer { constructor(vio: Vio, opts?: VioHttpServerOption); @@ -313,11 +383,86 @@ export class VioHttpServer { // @public (undocumented) export interface VioHttpServerOption { + frontendConfig?: object; + requestHandler?: (request: Request) => Response | undefined | Promise; + rpcAuthenticate?(request: Request): void; + // @deprecated (undocumented) staticHandler?: (request: Request) => Response | undefined | Promise; staticSetHeaders?: Record; vioStaticDir?: string; } +// @public (undocumented) +export interface VioObject { + // (undocumented) + readonly id: number; + // (undocumented) + name?: string; + // (undocumented) + readonly type: string; +} + +// @public (undocumented) +export interface VioObjectCenter { + // (undocumented) + chartsNumber: number; + // (undocumented) + disposeObject(object: VioObject): void; + getAll(): IterableIterator; +} + +// @public (undocumented) +export interface VioObjectCenter { + // @deprecated + create(dimension: 1, options?: ChartCreateOption): VioChart; + // @deprecated + create(dimension: 2, options?: ChartCreateOption): VioChart; + // @deprecated + create(dimension: 3, options?: ChartCreateOption): VioChart; + // @deprecated (undocumented) + create(dimension: number, options?: ChartCreateOption): VioChart; + createChart(dimension: 1, options?: ChartCreateOption): VioChart; + createChart(dimension: 2, options?: ChartCreateOption): VioChart; + createChart(dimension: 3, options?: ChartCreateOption): VioChart; + // (undocumented) + createChart(dimension: number, options?: ChartCreateOption): VioChart; + // @deprecated (undocumented) + disposeChart(chart: VioChart): void; + // @deprecated (undocumented) + get(chartId: number): VioChart | undefined; +} + +// @public (undocumented) +export interface VioObjectCenter { + // (undocumented) + createTable(columns: Column[], option?: TableCreateOption): VioTable; +} + +// @public (undocumented) +export interface VioTable extends VioObject { + addRow(row: Row, afterIndex?: number): void; + deleteRow(index: number, count: number): void; + // (undocumented) + getRow(index: number): Row; + // (undocumented) + getRowIndexByKey(key: Key): number; + // (undocumented) + getRows(filter?: TableFilter): { + rows: Row[]; + index: number[]; + }; + onRowAction(operateKey: string, rowKey: Key): void; + onRowAdd(param: Add): void; + onRowUpdate(rowKey: string, param: Update): void; + onTableAction(operateKey: string, rowKeys: Key[]): void; + rowNumber: number; + // (undocumented) + readonly type: "table"; + // (undocumented) + updateRow(row: Row, index: number): void; + updateTable(data: Row[]): void; +} + // @public (undocumented) export interface VioTty extends TTY { cachedSize: number; @@ -326,10 +471,6 @@ export interface VioTty extends TTY { getCache(): IterableIterator; } -// Warnings were encountered during analysis: -// -// dist/mod.d.ts:309:5 - (ae-forgotten-export) The symbol "DimensionInfo" needs to be exported by the entry point index.d.ts - // (No @packageDocumentation comment for this package) ``` diff --git a/vio/build/rollup.config.js b/vio/build/rollup.config.js index 3182b42..4fb316f 100644 --- a/vio/build/rollup.config.js +++ b/vio/build/rollup.config.js @@ -42,6 +42,7 @@ export default defineEvConfig({ ], extra: { typescript: { + tsconfig: "./tsconfig.build.json", compilerOptions: { target: "ES2022", module: "NodeNext", diff --git a/vio/deno.json b/vio/deno.json index ef21ed4..65b2447 100644 --- a/vio/deno.json +++ b/vio/deno.json @@ -1,6 +1,6 @@ { "name": "@asla/vio", - "version": "0.1.1", + "version": "0.2.0", "exports": "./src/mod.deno.ts", "tasks": { "gen-doc": "deno doc --html --output=temp --name=VIO src/mod.ts", @@ -10,7 +10,7 @@ "@asla/vio": "./src/mod.deno.ts", "jbod": "jsr:@asn/jbod@^0.5.0", "evlib": "jsr:@asn/evlib@^2.6.1", - "cpcall": "jsr:@asn/cpcall@^0.6.0" + "cpcall": "jsr:@asn/cpcall@^0.6.2" }, "compilerOptions": { "lib": ["deno.window"] diff --git a/vio/examples/cases/chart.ts b/vio/examples/cases/chart.ts index 6c76b11..a44bf67 100644 --- a/vio/examples/cases/chart.ts +++ b/vio/examples/cases/chart.ts @@ -9,7 +9,8 @@ function getMemoryChartData() { /** 内存图。每两秒更新一次图 */ export async function memoryChart(vio: Vio) { - const chart = vio.chart.create(2, { + const chart = vio.object.createChart(2, { + name: "内存", meta: { chartType: "line", //折线图 title: "内存", // 图表标题 diff --git a/vio/examples/cases/table.ts b/vio/examples/cases/table.ts new file mode 100644 index 0000000..2213521 --- /dev/null +++ b/vio/examples/cases/table.ts @@ -0,0 +1,112 @@ +import { Column, UiButton, Vio, VioTable, Key } from "@asla/vio"; + +type Process = { + swapFile: string; + args: string[]; + createTime: number; + status: 0 | 1; + id: string; +}; + +const columns: Column[] = [ + { dataIndex: "swapFile" }, + { + dataIndex: "args", + }, + { title: "创建事件", dataIndex: "createTime" }, + { + title: "状态", + dataIndex: "status", + render: ` + const { record } = args; + const status = record.status; + return { + key: record.status ? "stop" : "run", + ui: "tag", + props: { text: status ? "running" : "stopped", color: status ? "green" : "red" }, + }; + `, + }, + { + title: "操作", + render: ` + const { record } = args; + return [ + { key: "stop", ui: "button", props: { disable: record.status === 0, text: "停止", type: "text" } }, + { key: "run", ui: "button", props: { disable: record.status === 1, text: "启动", type: "text" } }, + ]; + `, + }, +]; + +export function appendTable(vio: Vio) { + const t1 = vio.object.createTable(columns, { + keyField: "id", + name: "只读表格", + }); + let rows: Process[] = []; + for (let i = 0; i < 100; i++) { + rows[i] = createRow("ps-" + i); + } + t1.updateTable(rows); + const t2 = vio.object.createTable(columns, { + keyField: "id", + name: "可编辑表格", + addAction: { text: "添加进程" }, + updateAction: true, + operations: [ + new UiButton("run", { + text: "批量启动", + icon: "https://search-operate.cdn.bcebos.com/e8cbce1d53432a6950071bf26b640e2b.gif", + }), + new UiButton("stop", { text: "批量停止" }), + ], + }); + t2.addRow(createRow("p2-1")); + t2.addRow(createRow("p2-2")); + t2.addRow(createRow("p2-4")); + + const onAction = function (this: VioTable, opKey: string, rowKey: Key) { + const index = this.getRowIndexByKey(rowKey); + const row = this.getRow(index); + + switch (opKey) { + case "run": + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + this.updateRow({ ...row, status: 1 }, index); + }, 1000); + }); + case "stop": + this.updateRow({ ...row, status: 0 }, index); + break; + default: + break; + } + }; + const onTableAction = function (this: VioTable, opKey: string, selectedKeys: Key[]) { + const status = opKey === "run" ? 1 : 0; + for (const key of selectedKeys) { + const index = this.getRowIndexByKey(key); + const row = this.getRow(index); + this.updateRow({ ...row, status }, index); + } + }; + t1.onRowAction = onAction; + t2.onRowAction = onAction; + + t1.onTableAction = onTableAction; + t2.onTableAction = onTableAction; + + return { t1, t2 }; +} +function createRow(id: string): Process { + return { + args: ["-a"], + swapFile: "xxx" + id, + createTime: Date.now(), + id, + status: Math.random() > 0.5 ? 1 : 0, + }; +} diff --git a/vio/examples/run_server.ts b/vio/examples/run_server.ts index 9ad718a..3c0648b 100644 --- a/vio/examples/run_server.ts +++ b/vio/examples/run_server.ts @@ -2,10 +2,12 @@ import vio, { Vio, VioHttpServer } from "@asla/vio"; import { inputSelect } from "./cases/action.ts"; import { intervalOutput } from "./cases/output.ts"; import { memoryChart } from "./cases/chart.ts"; +import { appendTable } from "./cases/table.ts"; export async function startDefaultServer(vio: Vio, port: number = 8887, hostname: string = "127.0.0.1") { intervalOutput(vio.tty.get(0)); inputSelect(vio.tty.get(1)); memoryChart(vio); + appendTable(vio); const server = new VioHttpServer(vio); await server.listen(port, hostname); diff --git a/vio/package.json b/vio/package.json index 92b7a76..a63ea84 100644 --- a/vio/package.json +++ b/vio/package.json @@ -1,6 +1,6 @@ { "name": "@asla/vio", - "version": "0.1.1", + "version": "0.2.0", "description": "A web terminal that provides a variety of graphical controls. You can interact with processes in the browser", "type": "module", "scripts": { @@ -15,12 +15,12 @@ "license": "MIT", "devDependencies": { "@eavid/lib-node": "^2.1.2", - "@types/node": "^20.14.10", + "@types/node": "^22.0.2", "evlib": "^2.6.1", "rollup-plugin-dts": "^6.1.1" }, "dependencies": { - "cpcall": "^0.6.0" + "cpcall": "^0.6.2" }, "publishConfig": { "access": "public", diff --git a/vio/src/client.ts b/vio/src/client.ts index d95ef1d..d098ea9 100644 --- a/vio/src/client.ts +++ b/vio/src/client.ts @@ -1,2 +1,3 @@ export type * from "./vio/api_type.ts"; -export * from "./vio/classes/VioChart.ts"; +export * from "./vio/vio_object/mod.private.ts"; +export * from "./vio/tty/mod.private.ts"; diff --git a/vio/src/const.ts b/vio/src/const.ts index ebbfe05..a4c0e87 100644 --- a/vio/src/const.ts +++ b/vio/src/const.ts @@ -1,11 +1,5 @@ import path from "node:path"; -export class InstanceDisposedError extends Error { - constructor(name: string = "Instance") { - super(`${name} has been disposed`); - } -} - // 输出目录保持 export const packageDir = (function () { diff --git a/vio/src/lib/serve/HttpServer.ts b/vio/src/lib/serve/HttpServer.ts index 00f9cff..02b8745 100644 --- a/vio/src/lib/serve/HttpServer.ts +++ b/vio/src/lib/serve/HttpServer.ts @@ -108,6 +108,7 @@ export class HttpServer implements DenoHttpServer { shutdown(): Promise { if (this.#closing) return this.finished; this.#server.close(); + this.#server.closeAllConnections(); return this.finished; } diff --git a/vio/src/mod.node.ts b/vio/src/mod.node.ts index 356a7b5..c80497d 100644 --- a/vio/src/mod.node.ts +++ b/vio/src/mod.node.ts @@ -5,7 +5,7 @@ import { platformApi } from "./server/platform_api.ts"; if (runtimeEngine === "deno") { // deno 可能导入 npm:@asla/vio - const mod = await import("./compat/platform_api.deno.ts"); + const mod = await import("./compat/platform_api.deno.ts" as string); // 跳过类型检查 Object.assign(platformApi, mod.default); } else { const mod = await import("./compat/platform_api.node.ts"); diff --git a/vio/src/rpc/ClientObjectApi.ts b/vio/src/rpc/ClientObjectApi.ts new file mode 100644 index 0000000..587c1ad --- /dev/null +++ b/vio/src/rpc/ClientObjectApi.ts @@ -0,0 +1,38 @@ +import { CpCall, MakeCallers } from "cpcall"; +import type { + VioClientExposed, + VioObjectCreateDto, + ChartUpdateData, + ClientObjectExposed, + TableChanges, +} from "../vio/api_type.ts"; +import { Key, TableRow } from "../vio/mod.ts"; + +export class ClientObjectApi implements ClientObjectExposed { + constructor(api: MakeCallers) { + this.#api = api.object; + } + #api?: MakeCallers; + createObject(info: VioObjectCreateDto): void { + if (!this.#api) return; + CpCall.exec(this.#api.createObject, info); + } + deleteObject(id: number): void { + if (!this.#api) return; + CpCall.exec(this.#api.deleteObject, id); + } + + writeChart(id: number, data: ChartUpdateData): void { + if (!this.#api) return; + CpCall.exec(this.#api.writeChart, id, data); + } + + updateTable(tableId: number): void { + if (!this.#api) return; + CpCall.exec(this.#api.updateTable, tableId); + } + tableChange(tableId: number, changes: TableChanges): void { + if (!this.#api) return; + CpCall.exec(this.#api.tableChange, tableId, changes); + } +} diff --git a/vio/src/rpc/ClientTtyApi.ts b/vio/src/rpc/ClientTtyApi.ts new file mode 100644 index 0000000..7200f25 --- /dev/null +++ b/vio/src/rpc/ClientTtyApi.ts @@ -0,0 +1,21 @@ +import { CpCall, MakeCallers } from "cpcall"; +import { ClientTtyExposed, TtyInputsReq, TtyOutputsData, VioClientExposed } from "../vio/api_type.ts"; + +export class ClientTtyApi implements ClientTtyExposed { + constructor(api: MakeCallers) { + this.#api = api; + } + #api?: MakeCallers; + sendTtyReadRequest(ttyId: number, requestId: number, data: TtyInputsReq) { + if (!this.#api) return Promise.reject(new Error("Viewer has been disposed")); + CpCall.exec(this.#api.tty.sendTtyReadRequest, ttyId, requestId, data); + } + writeTty(id: number, data: TtyOutputsData | TtyOutputsData): void { + if (!this.#api) throw new Error("Viewer has been disposed"); + CpCall.exec(this.#api.tty.writeTty, id, data); + } + ttyReadEnableChange(ttyId: number, enable: boolean): void { + if (!this.#api) return; + CpCall.exec(this.#api.tty.ttyReadEnableChange, ttyId, enable); + } +} diff --git a/vio/src/rpc/mod.ts b/vio/src/rpc/mod.ts new file mode 100644 index 0000000..1023693 --- /dev/null +++ b/vio/src/rpc/mod.ts @@ -0,0 +1,3 @@ +export * from "./ClientObjectApi.ts"; +export * from "./ClientTtyApi.ts"; +export * from "./rpc_api.ts"; diff --git a/vio/src/rpc/rpc_api.ts b/vio/src/rpc/rpc_api.ts index ddde2a5..6f319a6 100644 --- a/vio/src/rpc/rpc_api.ts +++ b/vio/src/rpc/rpc_api.ts @@ -1,141 +1,26 @@ -import { CpCall, MakeCallers, createWebSocketCpc } from "cpcall"; -import type { - VioClientExposed, - ChartInfo, - TtyOutputsData, - VioServerExposed, - ChartCreateInfo, - ChartUpdateData, - TtyInputsReq, -} from "../vio/api_type.ts"; -import { TtyCenter, ChartCenter, Vio } from "../vio/mod.ts"; -import { RequestUpdateRes, TtyReadResolver, VioChart } from "../vio/classes/mod.ts"; +import { CpCall, createWebSocketCpc } from "cpcall"; +import type { VioClientExposed, VioServerExposed } from "../vio/api_type.ts"; +import { Vio } from "../vio/mod.ts"; import type { WebSocket } from "../lib/deno/http.ts"; -import { MaybePromise } from "../type.ts"; -import { indexRecordToArray } from "../lib/array_like.ts"; -function getChartInfo(chart: VioChart): ChartInfo { - const cacheData: T[] = new Array(chart.cachedSize); - const timestamps: number[] = new Array(chart.cachedSize); - let i = 0; - for (const item of chart.getCacheDateItem()) { - cacheData[i] = item.data; - timestamps[i] = item.timestamp; - i++; - } - return { - meta: chart.meta, - dimension: chart.dimension, - id: chart.id, - cacheList: Array.from(chart.getCacheDateItem()), - dimensions: indexRecordToArray(chart.dimensions), - }; -} -class RpcServerExposed implements VioServerExposed { - constructor(vio: { chart: ChartCenter; tty: TtyCenter }, clientApi: RpcClientApi) { - this.#clientApi = clientApi; - this.#vio = vio; - } - #clientApi: RpcClientApi; - #vio: { chart: ChartCenter; tty: TtyCenter }; - getCharts(): { list: ChartInfo[] } { - const list: ChartInfo[] = new Array(this.#vio.chart.chartsNumber); - let i = 0; - for (const chart of this.#vio.chart.getAll()) { - list[i++] = getChartInfo(chart); - } - return { list }; - } - getChartInfo(id: number): ChartInfo | undefined { - const chart = this.#vio.chart.get(id); - if (!chart) return; - return getChartInfo(chart); - } - requestUpdateChart(chartId: number): MaybePromise> { - return this.#vio.chart.requestUpdate(chartId); - } +import { RpcServerObjectExposed, VioObjectCenterImpl } from "../vio/vio_object/mod.private.ts"; +import { RpcServerTtyExposed } from "../vio/tty/mod.private.ts"; +import { ClientTtyApi } from "./ClientTtyApi.ts"; +import { ClientObjectApi } from "./ClientObjectApi.ts"; - getTtyCache(id: number): TtyOutputsData[] { - const tty = this.#vio.tty.getCreated(id); - if (!tty) return []; - return Array.from(tty.getCache()); - } - resolveTtyReadRequest(ttyId: number, requestId: number, res: any): boolean { - const hd = this.#resolverMap[ttyId]; - if (!hd) return false; - return hd.resolve(requestId, res); - } - rejectTtyReadRequest(ttyId: number, requestId: number, reason: any): boolean { - const hd = this.#resolverMap[ttyId]; - if (!hd) return false; - return hd.reject(requestId, reason); - } - inputTty(ttyId: number, data: any): boolean { - const resolver = this.#resolverMap[ttyId]; - if (!resolver) return false; - return resolver.input(data); - } - /** 某个连接中开启读取权的 tty 字典 */ - #resolverMap: Record = {}; - setTtyReadEnable(ttyId: number, enable: boolean): boolean { - let resolver = this.#resolverMap[ttyId]; - if (enable) { - if (resolver) return true; - else { - resolver = this.#vio.tty.setReader(ttyId, { - read: (ttyId, requestId, data) => this.#clientApi.sendTtyReadRequest(ttyId, requestId, data), - dispose: () => { - if (this.#resolverMap[ttyId]) { - delete this.#resolverMap[ttyId]; - this.#clientApi.ttyReadEnableChange(ttyId, false); - } - }, - }); - } - this.#resolverMap[ttyId] = resolver; - } else { - if (resolver) { - delete this.#resolverMap[ttyId]; // 主动关闭,dispose 之前删除, - resolver.dispose(); - } - } - return true; - } -} -class RpcClientApi implements VioClientExposed { - constructor(api: MakeCallers) { - this.#api = api; - } - #api?: MakeCallers; - sendTtyReadRequest(ttyId: number, requestId: number, data: TtyInputsReq) { - if (!this.#api) return Promise.reject(new Error("Viewer has been disposed")); - CpCall.exec(this.#api.sendTtyReadRequest, ttyId, requestId, data); - } - writeTty(id: number, data: TtyOutputsData | TtyOutputsData): void { - if (!this.#api) throw new Error("Viewer has been disposed"); - CpCall.exec(this.#api.writeTty, id, data); - } - createChart(chart: ChartCreateInfo): void { - if (!this.#api) return; - CpCall.exec(this.#api.createChart, chart); - } - deleteChart(id: number): void { - if (!this.#api) return; - CpCall.exec(this.#api.deleteChart, id); - } - writeChart(id: number, data: ChartUpdateData): void { - if (!this.#api) return; - CpCall.exec(this.#api.writeChart, id, data); - } - ttyReadEnableChange(ttyId: number, enable: boolean): void { - if (!this.#api) return; - CpCall.exec(this.#api.ttyReadEnableChange, ttyId, enable); - } -} +export type RpcClientApi = { + tty: ClientTtyApi; + object: ClientObjectApi; +}; -export function initWebsocket(vio: Vio, ws: WebSocket): { cpc: CpCall; clientApi: VioClientExposed } { +export function initWebsocket(vio: Vio, ws: WebSocket): { cpc: CpCall; clientApi: RpcClientApi } { const cpc = createWebSocketCpc(ws); - const clientApi = new RpcClientApi(cpc.genCaller()); - cpc.setObject(new RpcServerExposed(vio, clientApi)); + const caller = cpc.genCaller(); + const objectApi = new ClientObjectApi(caller); + const ttyApi = new ClientTtyApi(caller); + cpc.exposeObject({ + object: new RpcServerObjectExposed(vio.object as VioObjectCenterImpl), + tty: new RpcServerTtyExposed(vio.tty, ttyApi), + } satisfies VioServerExposed); - return { clientApi, cpc }; + return { clientApi: { object: objectApi, tty: ttyApi }, cpc }; } diff --git a/vio/src/server/rpc_vio_server.ts b/vio/src/server/rpc_vio_server.ts index 89d6862..9e8f131 100644 --- a/vio/src/server/rpc_vio_server.ts +++ b/vio/src/server/rpc_vio_server.ts @@ -6,7 +6,6 @@ import { packageDir } from "../const.ts"; import path from "node:path"; import { HttpServer, ServeHandlerInfo } from "../lib/deno/http.ts"; import { platformApi } from "./platform_api.ts"; - /** * @public */ @@ -15,8 +14,17 @@ export interface VioHttpServerOption { vioStaticDir?: string; /** 覆盖静态资源响应头 */ staticSetHeaders?: Record; - /** 自定义处理静态资源请求。如果设置了这个处理函数,将忽略 vioStaticDir 和 staticSetHeaders */ + /** + * @deprecated 改用 requestHandler + * 自定义处理请求。请求处理在 api 之后,静态文件服务器之前。 + */ staticHandler?: (request: Request) => Response | undefined | Promise; + /** 自定义处理请求。请求处理在 api 之后,静态文件服务器之前。 */ + requestHandler?: (request: Request) => Response | undefined | Promise; + /** web终端前端配置 */ + frontendConfig?: object; + /** 连接鉴权. 如果不通过,应抛出异常 */ + rpcAuthenticate?(request: Request): void; } /** * vio http 服务器 @@ -27,43 +35,66 @@ export class VioHttpServer { private vio: Vio, opts: VioHttpServerOption = {}, ) { - let { vioStaticDir, staticSetHeaders, staticHandler } = opts; + let { vioStaticDir, staticSetHeaders, staticHandler, requestHandler = staticHandler, rpcAuthenticate } = opts; if (!vioStaticDir && packageDir) { vioStaticDir = path.resolve(packageDir, "assets/web"); } - if (staticHandler) { - this.#staticHandler = staticHandler; - } else if (vioStaticDir) { - const staticHandler = new FileServerHandler(vioStaticDir, { setHeaders: staticSetHeaders }); - this.#staticHandler = (request) => { - return staticHandler.getResponse(new URL(request.url).pathname, request.headers); - }; + if (requestHandler) { + this.#customHandler = requestHandler; + } + if (vioStaticDir) { + this.#fileServerHandler = new FileServerHandler(vioStaticDir, { setHeaders: staticSetHeaders }); + } + if (opts.frontendConfig) { + this.#frontendConfig = Response.json(opts.frontendConfig); } + if (typeof opts.rpcAuthenticate === "function") this.#rpcAuthenticate = opts.rpcAuthenticate; const router = new Router(); router.set("/api/test", function () { - const res = { value: "ok" }; - return new Response(JSON.stringify(res)); + return Response.json({ value: "ok" }); }); router.set("/api/rpc", ({ request: req }) => { if (req.headers.get("Upgrade") !== "websocket") return new Response(undefined, { status: 400 }); + if (this.#rpcAuthenticate) { + try { + this.#rpcAuthenticate(req); + } catch (error) { + return new Response(null, { status: 403 }); + } + } + const { response, socket: websocket } = platformApi.upgradeWebSocket(req); - this.#ontWebSocketConnect(websocket); + this.#onWebSocketConnect(websocket); return response; }); this.#router = router; + this.#rpcAuthenticate; } - #staticHandler: VioHttpServerOption["staticHandler"]; + + #customHandler: VioHttpServerOption["requestHandler"]; + #fileServerHandler?: FileServerHandler; + #frontendConfig?: Response; #handler = async (req: Request, info: ServeHandlerInfo) => { const context = createRequestContext(req, info); const pathname = context.url.pathname; - const handler = this.#router.get(pathname); - if (handler) { - return handler(context); + if (pathname.startsWith("/api")) { + const handler = this.#router.get(pathname); + if (handler) { + return handler(context); + } + return new Response(null, { status: 404 }); } - if (this.#staticHandler) { - const response = await this.#staticHandler(req); + if (pathname === "/config.json" && this.#frontendConfig) { + return this.#frontendConfig; + } + if (this.#customHandler) { + const response = await this.#customHandler(req); + if (response) return response; + } + if (this.#fileServerHandler) { + const response = await this.#fileServerHandler.getResponse(pathname, req.headers); if (response) return response; } return new Response(null, { status: 404 }); @@ -71,7 +102,9 @@ export class VioHttpServer { #serve?: HttpServer; #router: Router; - async #ontWebSocketConnect(ws: WebSocket): Promise { + + #rpcAuthenticate?: (request: Request) => void; + async #onWebSocketConnect(ws: WebSocket): Promise { if (!this.#serve) throw new Error("unable connect"); if (ws.readyState === ws.CONNECTING) { let listener: (...args: any[]) => void; diff --git a/vio/src/vio/api_type.ts b/vio/src/vio/api_type.ts index 1de6218..48d0017 100644 --- a/vio/src/vio/api_type.ts +++ b/vio/src/vio/api_type.ts @@ -1,44 +1,15 @@ -export * from "./api_type/chart.type.ts"; -export * from "./api_type/tty.type.ts"; +export * from "./tty/tty.dto.ts"; +export * from "./vio_object/object.dto.ts"; -import { MaybePromise } from "../type.ts"; -import type { ChartCreateInfo, ChartInfo, ChartUpdateData, RequestUpdateRes } from "./api_type/chart.type.ts"; -import type { TtyOutputsData, TtyInputsReq } from "./api_type/tty.type.ts"; +import type { ServerTtyExposed, ClientTtyExposed } from "./tty/tty.dto.ts"; +import type { ClientObjectExposed, ServerObjectExposed } from "./vio_object/object.dto.ts"; -export interface VioClientExposed extends ChartController { - /** 在指定 TTY 输出数据 */ - writeTty(ttyId: number, data: TtyOutputsData): void; - /** 在指定 TTY 发送读取请求 */ - sendTtyReadRequest(ttyId: number, requestId: number, opts: TtyInputsReq): void; - /** 切换 TTY 读取权限 */ - ttyReadEnableChange(ttyId: number, enable: boolean): void; -} - -export interface ChartController { - /** 在指定图表输出数据 */ - writeChart(chartId: number, data: Readonly>): void; - /** 删除指定图表 */ - deleteChart(chartId: number): void; - /** 创建图表 */ - createChart(chartInfo: ChartCreateInfo): void; +export interface VioClientExposed { + object: ClientObjectExposed; + tty: ClientTtyExposed; } export interface VioServerExposed { - /** 获取所有图表的信息 */ - getCharts(): MaybePromise<{ list: ChartInfo[] }>; - /** 获取指定图表的信息 */ - getChartInfo(chartId: number): MaybePromise; - /** 主动请求更新图 */ - requestUpdateChart(chartId: number): MaybePromise>; - - /** 获取 TTY 输出缓存日志 */ - getTtyCache(ttyId: number): MaybePromise; - /** 切换 TTY 读取权限 */ - setTtyReadEnable(ttyId: number, enable: boolean): MaybePromise; - /** 解决 tty 输入请求 */ - resolveTtyReadRequest(ttyId: number, requestId: number, res: any): MaybePromise; - /** 拒绝 tty 输入请求 */ - rejectTtyReadRequest(ttyId: number, requestId: number, reason?: any): MaybePromise; - - inputTty(ttyId: number, data: any): MaybePromise; + object: ServerObjectExposed; + tty: ServerTtyExposed; } diff --git a/vio/src/vio/classes/chart.ts b/vio/src/vio/classes/chart.ts deleted file mode 100644 index be4b1c8..0000000 --- a/vio/src/vio/classes/chart.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { - ChartUpdateData, - ChartUpdateSubData, - DimensionalityReduction, - IntersectingDimension, - RequestUpdateRes, -} from "../api_type/chart.type.ts"; -import { UniqueKeyMap } from "evlib/data_struct"; -import { deepClone } from "evlib"; -import { ChartUpdateLowerOption, ChartUpdateOption, VioChart, VioChartImpl, ChartCreateOption } from "./VioChart.ts"; -import { indexRecordToArray } from "../../lib/array_like.ts"; -import { ChartController } from "../api_type.ts"; -import { MaybePromise } from "../../type.ts"; - -/** - * @public - * @category Chart - */ -export class ChartCenter { - /** 图表默认缓存数量 */ - static TTY_DEFAULT_CACHE_SIZE = 20; - constructor(private ctrl: ChartController) {} - #instanceMap = new UniqueKeyMap(2 ** 32); - /** 获取指定索引的 Chart */ - get(chartId: number): VioChart | undefined { - return this.#instanceMap.get(chartId); - } - /** 获取所有已创建的 Chart */ - getAll(): IterableIterator> { - return this.#instanceMap.values(); - } - /** 所有已创建图表的数量 */ - get chartsNumber(): number { - return this.#instanceMap.size; - } - - /** 创建一维图表 */ - create(dimension: 1, options?: CenterCreateChartOption): VioChart; - /** 创建二维图表 */ - create(dimension: 2, options?: CenterCreateChartOption): VioChart; - /** 创建三维图表 */ - create(dimension: 3, options?: CenterCreateChartOption): VioChart; - create(dimension: number, options?: CenterCreateChartOption): VioChart; - create(dimension: number, options?: CenterCreateChartOption): VioChart { - let chartId = this.#instanceMap.allocKeySet(null as any); - const chart = new ChartCenter.Chart(this, chartId, dimension, options); - this.#instanceMap.set(chartId, chart); - this.ctrl.createChart({ - meta: chart.meta, - dimension: chart.dimension, - id: chartId, - dimensions: indexRecordToArray(chart.dimensions), - }); - - return chart; - } - /** web 端主动请求更新图表,这会触发 chart.onRequestUpdate() */ - requestUpdate(chartId: number): MaybePromise> { - const chart = this.#instanceMap.get(chartId); - if (!chart) throw new Error(`Chart '${chartId}' dest not exist`); - return chart.onUpdate() as MaybePromise>; - } - /** 销毁图表 */ - disposeChart(chart: VioChart) { - if (!(chart instanceof ChartCenter.Chart)) throw new Error("This chart does not belong to the center"); - chart.dispose(); - } - private static Chart = class RpcVioChart extends VioChartImpl { - constructor(center: ChartCenter, chartId: number, dimension: number, options: CenterCreateChartOption = {}) { - const { maxCacheSize = ChartCenter.TTY_DEFAULT_CACHE_SIZE } = options; - super({ - ...options, - id: chartId, - dimension, - maxCacheSize, - }); - this.#center = center; - } - #lastData?: MaybePromise>; - - onUpdate(): MaybePromise> { - if (!this.onRequestUpdate) throw new Error("Requests for updates are not allowed"); - const timestamp = Date.now(); - - if (this.#lastData) { - if (this.#lastData instanceof Promise) return this.#lastData; - if (timestamp - this.#lastData.timestamp <= this.updateThrottle) return this.#lastData; - } - - const value = this.onRequestUpdate(); - let res: MaybePromise>; - if (value instanceof Promise) - res = value.then( - (value): Readonly> => { - let res = { data: value, timestamp: timestamp } as const; - this.pushCache({ data: value, timestamp }); - this.#lastData = res; - return res; - }, - (e) => { - this.#lastData = undefined; - throw e; - }, - ); - else { - res = { data: value, timestamp: timestamp }; - this.pushCache({ data: value, timestamp }); - } - this.#lastData = res; - return res; - } - #center?: ChartCenter; - /** @override */ - updateData(data: T, timeName?: string): void { - if (!this.#center) return; - let internalData = typeof data === "object" ? deepClone(data) : data; - - const timestamp = Date.now(); - this.pushCache({ data: internalData, timestamp, timeName }); - - const writeData: ChartUpdateData = { data: internalData, timestamp: timestamp, timeAxisName: timeName }; - this.#center.ctrl.writeChart(this.id, writeData); - } - updateSubData(updateData: DimensionalityReduction, coord: number, opts?: ChartUpdateLowerOption): void; - updateSubData(updateData: IntersectingDimension, coord: (number | undefined)[], opts?: ChartUpdateOption): void; - /** @override */ - updateSubData( - updateData: IntersectingDimension, - coord: number | (number | undefined)[], - opts?: ChartUpdateLowerOption | ChartUpdateOption, - ): void { - if (!this.#center) return; - const timestamp = Date.now(); - this.#center.ctrl.writeChart(this.id, { - data: updateData, - coord: coord as any, - timeAxisName: opts?.timeName, - timestamp: timestamp, - } satisfies ChartUpdateSubData); - if (this.maxCacheSize <= 0) return; - //@ts-ignore - super.updateSubData(updateData, coord, opts); - } - get disposed() { - return !this.#center; - } - dispose() { - if (!this.#center) return; - const center = this.#center; - this.#center = undefined; - const id = this.id; - center.#instanceMap.delete(id); - center.ctrl.deleteChart(id); - } - }; -} - -type RpcVioChart = InstanceType<(typeof ChartCenter)["Chart"]>; - -export type { - ChartInfo, - DimensionalityReduction, - IntersectingDimension, - ChartMeta, - VioChartMeta, - VioChartType, - RequestUpdateRes, - ChartDataItem, -} from "../api_type/chart.type.ts"; - -/** - * VioChart 构造函数的选项 - * @public - * @category Chart - */ -export type CenterCreateChartOption = ChartCreateOption & { - /** 主动请求更新的回调函数 */ - onRequestUpdate?(): MaybePromise; - /** 请求更新节流。单位毫秒 */ - updateThrottle?: number; -}; diff --git a/vio/src/vio/classes/mod.ts b/vio/src/vio/classes/mod.ts deleted file mode 100644 index dea1404..0000000 --- a/vio/src/vio/classes/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./vio_tty.ts"; -export * from "./chart.ts"; -export * from "./VioChart.ts"; -export * from "./tty.ts"; diff --git a/vio/src/vio/const.ts b/vio/src/vio/const.ts new file mode 100644 index 0000000..44783a2 --- /dev/null +++ b/vio/src/vio/const.ts @@ -0,0 +1,5 @@ +export class InstanceDisposedError extends Error { + constructor(name: string = "Instance") { + super(`${name} has been disposed`); + } +} diff --git a/vio/src/vio/mod.ts b/vio/src/vio/mod.ts index 3c5498e..3f00412 100644 --- a/vio/src/vio/mod.ts +++ b/vio/src/vio/mod.ts @@ -1,16 +1,8 @@ import { Vio, createVio } from "./vio.ts"; export * from "./vio.ts"; -export type { - ChartCreateOption, - ChartUpdateLowerOption, - ChartUpdateOption, - VioChart, - VioChartCreateConfig, -} from "./classes/VioChart.ts"; -export * from "./classes/chart.ts"; -export * from "./classes/vio_tty.ts"; -export type { TTY, TTyWriteTextOption, TtyReadFileOption } from "./classes/tty.ts"; +export * from "./vio_object/mod.ts"; +export * from "./tty/mod.ts"; /** * @public */ diff --git a/vio/src/vio/classes/vio_tty.ts b/vio/src/vio/tty/TtyCenter.ts similarity index 96% rename from vio/src/vio/classes/vio_tty.ts rename to vio/src/vio/tty/TtyCenter.ts index 83a9a58..90d6f95 100644 --- a/vio/src/vio/classes/vio_tty.ts +++ b/vio/src/vio/tty/TtyCenter.ts @@ -1,11 +1,12 @@ import type { TtyInputsReq, TtyOutputsData, VioClientExposed } from "../api_type.ts"; import { LinkedQueue, UniqueKeyMap } from "evlib/data_struct"; -import { CacheTty, TTY } from "./tty.ts"; -import { InstanceDisposedError } from "../../const.ts"; +import { TTY } from "./_TTY.ts"; import { withPromise, WithPromise } from "evlib"; +import { CacheTty } from "./_CacheTty.ts"; +import { InstanceDisposedError } from "../const.ts"; type TtyWriterFn = (ttyId: number, data: TtyOutputsData) => void; -type TtyReadFn = VioClientExposed["sendTtyReadRequest"]; +type TtyReadFn = VioClientExposed["tty"]["sendTtyReadRequest"]; /** * @public @@ -283,12 +284,3 @@ export interface TtyReadResolver { /** 读取中的数量 */ waitingSize: number; } - -export type { - EncodedImageData, - RawImageData, - VioFileData as FileData, - SelectItem, - SelectKey, - TtyWriteTextType, -} from "../api_type/tty.type.ts"; diff --git a/vio/src/vio/tty/_CacheTty.ts b/vio/src/vio/tty/_CacheTty.ts new file mode 100644 index 0000000..ae32296 --- /dev/null +++ b/vio/src/vio/tty/_CacheTty.ts @@ -0,0 +1,46 @@ +import { LinkedCacheQueue } from "evlib/data_struct"; +import { TtyOutputsData } from "./tty.dto.ts"; +import { TTY } from "./_TTY.ts"; + +type WriteTty = (ttyId: number, data: TtyOutputsData) => void; +export abstract class CacheTty extends TTY { + constructor( + readonly ttyIndex: number, + cacheSize: number, + writeTty?: WriteTty, + ) { + super(); + this.#writeTty = writeTty; + this.#outputCache = new LinkedCacheQueue(cacheSize); + } + #writeTty?: WriteTty; + #outputCache: LinkedCacheQueue<{ data: TtyOutputsData }>; + get cachedSize() { + return this.#outputCache.size; + } + get cacheSize() { + return this.#outputCache.maxSize; + } + set cacheSize(size: number) { + this.#outputCache.maxSize = size; + } + *getCache(): Generator { + for (const item of this.#outputCache) { + yield item.data; + } + } + + /** @implements */ + write(data: TtyOutputsData): void { + this.#outputCache.push({ data }); + if (!this.#writeTty) return; + this.#writeTty(this.ttyIndex, data); + } + get disposed() { + return !this.#writeTty; + } + dispose() { + // dispose 后保留 cache + this.#writeTty = undefined; + } +} diff --git a/vio/src/vio/classes/tty.ts b/vio/src/vio/tty/_TTY.ts similarity index 77% rename from vio/src/vio/classes/tty.ts rename to vio/src/vio/tty/_TTY.ts index 148fcf2..74bec25 100644 --- a/vio/src/vio/classes/tty.ts +++ b/vio/src/vio/tty/_TTY.ts @@ -1,17 +1,13 @@ -import { LinkedCacheQueue } from "evlib/data_struct"; +import type { TtyOutputData, TtyInputsReq, TtyInputReq, TtyOutputsData } from "./tty.dto.ts"; import type { SelectItem, SelectKey, - TtyOutputData, - TtyInputsReq, - TtyInputReq, - TtyOutputsData, EncodedImageData, RawImageData, TtyWriteTextType, VioFileData, -} from "../api_type/tty.type.ts"; -import { VioChart, VioChartImpl } from "./VioChart.ts"; +} from "./type.ts"; +import type { VioObject } from "../vio_object/_object_base.type.ts"; import { createTypeErrorDesc } from "evlib"; /** @@ -81,8 +77,8 @@ export abstract class TTY { msgType, } satisfies TtyOutputData.Text); } - writeUiLink(ui: VioChart): void { - if (!(ui instanceof VioChartImpl)) throw new Error("Unsupported UI object"); + writeUiLink(ui: VioObject): void { + throw new Error("Unsupported UI object"); this.write({ type: "link", uiType: "chart", id: ui.id } satisfies TtyOutputData.UILink); } /** 读取任意数据 */ @@ -150,49 +146,6 @@ export abstract class TTY { } } -type WriteTty = (ttyId: number, data: TtyOutputsData) => void; -export abstract class CacheTty extends TTY { - constructor( - readonly ttyIndex: number, - cacheSize: number, - writeTty?: WriteTty, - ) { - super(); - this.#writeTty = writeTty; - this.#outputCache = new LinkedCacheQueue(cacheSize); - } - #writeTty?: WriteTty; - #outputCache: LinkedCacheQueue<{ data: TtyOutputsData }>; - get cachedSize() { - return this.#outputCache.size; - } - get cacheSize() { - return this.#outputCache.maxSize; - } - set cacheSize(size: number) { - this.#outputCache.maxSize = size; - } - *getCache(): Generator { - for (const item of this.#outputCache) { - yield item.data; - } - } - - /** @implements */ - write(data: TtyOutputsData): void { - this.#outputCache.push({ data }); - if (!this.#writeTty) return; - this.#writeTty(this.ttyIndex, data); - } - get disposed() { - return !this.#writeTty; - } - dispose() { - // dispose 后保留 cache - this.#writeTty = undefined; - } -} - class InvalidInputDataError extends Error { constructor(msg: string = "invalid input data") { super(msg); diff --git a/vio/src/vio/tty/_server_exposed.ts b/vio/src/vio/tty/_server_exposed.ts new file mode 100644 index 0000000..0d08ff0 --- /dev/null +++ b/vio/src/vio/tty/_server_exposed.ts @@ -0,0 +1,58 @@ +import { ClientTtyExposed, ServerTtyExposed, TtyInputsReq, TtyOutputsData } from "./tty.dto.ts"; +import { TtyCenter, TtyReadResolver } from "./TtyCenter.ts"; + +export class RpcServerTtyExposed implements ServerTtyExposed { + constructor(ttyCenter: TtyCenter, clientApi: ClientTtyExposed) { + this.#tty = ttyCenter; + this.#clientApi = clientApi; + } + #clientApi: ClientTtyExposed; + #tty: TtyCenter; + getTtyCache(id: number): TtyOutputsData[] { + const tty = this.#tty.getCreated(id); + if (!tty) return []; + return Array.from(tty.getCache()); + } + resolveTtyReadRequest(ttyId: number, requestId: number, res: any): boolean { + const hd = this.#resolverMap[ttyId]; + if (!hd) return false; + return hd.resolve(requestId, res); + } + rejectTtyReadRequest(ttyId: number, requestId: number, reason: any): boolean { + const hd = this.#resolverMap[ttyId]; + if (!hd) return false; + return hd.reject(requestId, reason); + } + inputTty(ttyId: number, data: any): boolean { + const resolver = this.#resolverMap[ttyId]; + if (!resolver) return false; + return resolver.input(data); + } + /** 某个连接中开启读取权的 tty 字典 */ + #resolverMap: Record = {}; + setTtyReadEnable(ttyId: number, enable: boolean): boolean { + let resolver = this.#resolverMap[ttyId]; + if (enable) { + if (resolver) return true; + else { + resolver = this.#tty.setReader(ttyId, { + read: (ttyId: number, requestId: number, data: TtyInputsReq) => + this.#clientApi.sendTtyReadRequest(ttyId, requestId, data), + dispose: () => { + if (this.#resolverMap[ttyId]) { + delete this.#resolverMap[ttyId]; + this.#clientApi.ttyReadEnableChange(ttyId, false); + } + }, + }); + } + this.#resolverMap[ttyId] = resolver; + } else { + if (resolver) { + delete this.#resolverMap[ttyId]; // 主动关闭,dispose 之前删除, + resolver.dispose(); + } + } + return true; + } +} diff --git a/vio/src/vio/tty/mod.private.ts b/vio/src/vio/tty/mod.private.ts new file mode 100644 index 0000000..39ab6e3 --- /dev/null +++ b/vio/src/vio/tty/mod.private.ts @@ -0,0 +1,5 @@ +export * from "./_TTY.ts"; +export * from "./TtyCenter.ts"; +export * from "./type.ts"; + +export * from "./_server_exposed.ts"; diff --git a/vio/src/vio/tty/mod.ts b/vio/src/vio/tty/mod.ts new file mode 100644 index 0000000..bc5bf1a --- /dev/null +++ b/vio/src/vio/tty/mod.ts @@ -0,0 +1,3 @@ +export * from "./_TTY.ts"; +export * from "./TtyCenter.ts"; +export * from "./type.ts"; diff --git a/vio/src/vio/api_type/tty.type.ts b/vio/src/vio/tty/tty.dto.ts similarity index 63% rename from vio/src/vio/api_type/tty.type.ts rename to vio/src/vio/tty/tty.dto.ts index 09f940b..41172bb 100644 --- a/vio/src/vio/api_type/tty.type.ts +++ b/vio/src/vio/tty/tty.dto.ts @@ -1,8 +1,5 @@ -/** - * @public - * @category TTY - */ -export type TtyWriteTextType = "warn" | "log" | "error" | "info"; +import { MaybePromise } from "../../type.ts"; +import { EncodedImageData, RawImageData, SelectItem, TtyWriteTextType, VioFileData } from "./type.ts"; /** 终端输出数据 */ export namespace TtyOutputData { @@ -74,42 +71,6 @@ export namespace TtyInputReq { } } -/** - * @public - * @category TTY - */ -export type RawImageData = { - /** 图像宽度 */ - width: number; - /** 图像高度 */ - height: number; - /** - * 图像二进制数据,必须满足 data.length === width * height * channel。 - * 数据由多个 width*height 长度的通道数据组成 - */ - data: Uint8Array; - /** 图像通道数 */ - channel: number; - separate?: boolean; -}; -/** - * 编码的图像数据,如 jpg、png等 - * @public - * @category TTY - */ -export type EncodedImageData = { - data: Uint8Array; - mime: string; -}; -/** - * @public - * @category TTY - */ -export type VioFileData = { - name: string; - data: Uint8Array; - mime: string; -}; export type TtyOutputsData = | TtyOutputData.Text | TtyOutputData.Table @@ -123,13 +84,24 @@ export type TtyInputsReq = | TtyInputReq.File | TtyInputReq.Select | TtyInputReq.Custom; -/** - * @public - * @category TTY - */ -export type SelectKey = string | number; -/** - * @public - * @category TTY - */ -export type SelectItem = { value: T; label?: string }; + +export interface ServerTtyExposed { + /** 获取 TTY 输出缓存日志 */ + getTtyCache(ttyId: number): MaybePromise; + /** 切换 TTY 读取权限 */ + setTtyReadEnable(ttyId: number, enable: boolean): MaybePromise; + /** 解决 tty 输入请求 */ + resolveTtyReadRequest(ttyId: number, requestId: number, res: any): MaybePromise; + /** 拒绝 tty 输入请求 */ + rejectTtyReadRequest(ttyId: number, requestId: number, reason?: any): MaybePromise; + + inputTty(ttyId: number, data: any): MaybePromise; +} +export interface ClientTtyExposed { + /** 在指定 TTY 输出数据 */ + writeTty(ttyId: number, data: TtyOutputsData): void; + /** 在指定 TTY 发送读取请求 */ + sendTtyReadRequest(ttyId: number, requestId: number, opts: TtyInputsReq): void; + /** 切换 TTY 读取权限 */ + ttyReadEnableChange(ttyId: number, enable: boolean): void; +} diff --git a/vio/src/vio/tty/type.ts b/vio/src/vio/tty/type.ts new file mode 100644 index 0000000..06fbe4a --- /dev/null +++ b/vio/src/vio/tty/type.ts @@ -0,0 +1,51 @@ +/** + * @public + * @category TTY + */ +export type TtyWriteTextType = "warn" | "log" | "error" | "info"; +/** + * @public + * @category TTY + */ +export type RawImageData = { + /** 图像宽度 */ + width: number; + /** 图像高度 */ + height: number; + /** + * 图像二进制数据,必须满足 data.length === width * height * channel。 + * 数据由多个 width*height 长度的通道数据组成 + */ + data: Uint8Array; + /** 图像通道数 */ + channel: number; + separate?: boolean; +}; +/** + * 编码的图像数据,如 jpg、png等 + * @public + * @category TTY + */ +export type EncodedImageData = { + data: Uint8Array; + mime: string; +}; +/** + * @public + * @category TTY + */ +export type VioFileData = { + name: string; + data: Uint8Array; + mime: string; +}; +/** + * @public + * @category TTY + */ +export type SelectKey = string | number; +/** + * @public + * @category TTY + */ +export type SelectItem = { value: T; label?: string }; diff --git a/vio/src/vio/type.ts b/vio/src/vio/type.ts new file mode 100644 index 0000000..e69de29 diff --git a/vio/src/vio/vio.ts b/vio/src/vio/vio.ts index d1e521c..3376a66 100644 --- a/vio/src/vio/vio.ts +++ b/vio/src/vio/vio.ts @@ -1,7 +1,8 @@ -import type { VioClientExposed, TtyInputsReq, TtyOutputsData } from "./api_type.ts"; +import type { TtyInputsReq, TtyOutputsData } from "./api_type.ts"; import type { WebSocket } from "../lib/deno/http.ts"; -import { initWebsocket } from "../rpc/rpc_api.ts"; -import { TtyCenter, ChartCenter, TTY, VioTty } from "./classes/mod.ts"; +import { ClientTtyApi, initWebsocket, type RpcClientApi } from "../rpc/mod.ts"; +import { TTY, TtyCenter, VioTty } from "./tty/mod.ts"; +import { VioObjectCenterImpl, VioObjectCenter } from "./vio_object/mod.private.ts"; /** VIO 实例。 * @public @@ -9,8 +10,13 @@ import { TtyCenter, ChartCenter, TTY, VioTty } from "./classes/mod.ts"; export interface Vio extends TTY { /** 终端相关的接口 */ readonly tty: TtyCenter; + /** + * 图相关的接口 + * @deprecated 改用 object + */ + readonly chart: VioObjectCenter; /** 图相关的接口 */ - readonly chart: ChartCenter; + readonly object: VioObjectCenter; // joinViewer(viewer: VioClientExposed, onDispose?: (viewer: Viewer) => void): Viewer; /** * 接入一个终端连接 @@ -26,7 +32,7 @@ class VioImpl extends TTY implements Vio { constructor() { super(); const writeTty = (ttyId: number, data: TtyOutputsData) => { - for (const viewer of this.#viewers.values()) viewer.writeTty(ttyId, data); + for (const viewer of this.#viewers.values()) viewer.tty.writeTty(ttyId, data); }; this.tty = new TtyCenter(writeTty); this.#tty0 = this.tty.get(0); @@ -38,9 +44,9 @@ class VioImpl extends TTY implements Vio { write(data: TtyOutputsData): void { return this.#tty0.write(data); } - readonly #viewers = new Map(); - joinViewer(api: VioClientExposed, onDispose?: (viewer: Viewer) => void): Viewer { - const viewer = new ViewerImpl(api, (viewer) => { + readonly #viewers = new Map(); + joinViewer(api: RpcClientApi, onDispose?: (viewer: Viewer) => void): Viewer { + const viewer = new ViewerImpl(api.tty, (viewer) => { this.#viewers.delete(viewer); onDispose?.(viewer); }); @@ -69,17 +75,24 @@ class VioImpl extends TTY implements Vio { } readonly tty: TtyCenter; - readonly chart: ChartCenter = new ChartCenter({ - writeChart: (chartId, data) => { - for (const viewer of this.#viewers.values()) viewer.writeChart(chartId, data); + readonly object = new VioObjectCenterImpl({ + createObject: (...args) => { + for (const viewer of this.#viewers.values()) viewer.object.createObject(...args); + }, + deleteObject: (...args) => { + for (const viewer of this.#viewers.values()) viewer.object.deleteObject(...args); }, - createChart: (config) => { - for (const viewer of this.#viewers.values()) viewer.createChart(config); + writeChart: (...args) => { + for (const viewer of this.#viewers.values()) viewer.object.writeChart(...args); }, - deleteChart: (chartId) => { - for (const viewer of this.#viewers.values()) viewer.deleteChart(chartId); + tableChange: (...args) => { + for (const viewer of this.#viewers.values()) viewer.object.tableChange(...args); + }, + updateTable: (...args) => { + for (const viewer of this.#viewers.values()) viewer.object.updateTable(...args); }, }); + readonly chart: VioObjectCenter = this.object; } /** * 创建 Vio 实例 @@ -92,10 +105,10 @@ export function createVio(): Vio { class ViewerImpl implements Viewer { constructor( - api: VioClientExposed, + ttyApi: ClientTtyApi, private onDispose: (viewer: ViewerImpl) => void, ) { - this.#api = api; + this.#api = ttyApi; } readTty(ttyId: number, reqId: number, config: TtyInputsReq) { this.#api?.sendTtyReadRequest(ttyId, reqId, config); @@ -103,7 +116,7 @@ class ViewerImpl implements Viewer { writeTty(ttyId: number, data: TtyOutputsData): void { this.#api?.writeTty(ttyId, data); } - #api?: VioClientExposed; + #api?: ClientTtyApi; dispose(): void { if (!this.#api) return; @@ -124,8 +137,3 @@ interface Viewer { readTty(ttyId: number, reqId: number, config: TtyInputsReq): void; writeTty(ttyId: number, data: TtyOutputsData): void; } - -/** @public */ -export type ClassToInterface = { - [key in keyof T]: T[key]; -}; diff --git a/vio/src/vio/vio_object/_VioObjectCenter.ts b/vio/src/vio/vio_object/_VioObjectCenter.ts new file mode 100644 index 0000000..428a47a --- /dev/null +++ b/vio/src/vio/vio_object/_VioObjectCenter.ts @@ -0,0 +1,67 @@ +import { VioChart as RpcVioChart } from "./chart/VioChart.ts"; +import { VioTableImpl } from "./table/VioTable.ts"; +import { UniqueKeyMap } from "evlib/data_struct"; +import type { VioChart, ChartCreateOption } from "./chart/chart.type.ts"; +import type { Column, TableCreateOption, TableRow, VioTable } from "./table/table.type.ts"; +import type { VioObjectCenter, VioObject } from "./object.type.ts"; +import type { ClientObjectExposed } from "./object.dto.ts"; + +export class VioObjectCenterImpl implements VioObjectCenter { + constructor(private ctrl: ClientObjectExposed) {} + defaultChartCacheSize = 20; + #instanceMap = new UniqueKeyMap(2 ** 32); + + getObject(objectId: number): VioObject | undefined { + return this.#instanceMap.get(objectId); + } + + /** 获取所有已 Vio 对象*/ + getAll(): IterableIterator { + return this.#instanceMap.values(); + } + /** + * 所有 Vio 对象数量 + * @deprecated 已废弃 + */ + get chartsNumber(): number { + return this.#instanceMap.size; + } + + disposeObject(chart: VioObject) { + if (chart instanceof RpcVioChart) { + chart.dispose(); + } else { + throw new Error("This chart does not belong to the center"); + } + this.#instanceMap.delete(chart.id); + } + + createChart(dimension: number, options?: ChartCreateOption): VioChart { + let chartId = this.#instanceMap.allocKeySet(null as any); + const chart = new RpcVioChart(this.ctrl, chartId, dimension, { + ...options, + maxCacheSize: options?.maxCacheSize ?? this.defaultChartCacheSize, + }); + this.#instanceMap.set(chartId, chart); + this.ctrl.createObject({ id: chartId, name: chart.name, type: chart.type }); + + return chart; + } + createTable(columns: Column[], option: TableCreateOption): VioTable { + let chartId = this.#instanceMap.allocKeySet(null as any); + const instance = new VioTableImpl(this.ctrl, chartId, columns, option); + this.#instanceMap.set(chartId, instance); + this.ctrl.createObject({ id: chartId, name: instance.name, type: instance.type }); + return instance; + } + + /** @deprecated 已废弃 */ + get(chartId: number): VioChart | undefined { + const obj = this.#instanceMap.get(chartId); + if (obj instanceof RpcVioChart) { + return obj; + } + } + create = this.createChart; + disposeChart = this.disposeObject; +} diff --git a/vio/src/vio/vio_object/_object_base.dto.ts b/vio/src/vio/vio_object/_object_base.dto.ts new file mode 100644 index 0000000..345d1ba --- /dev/null +++ b/vio/src/vio/vio_object/_object_base.dto.ts @@ -0,0 +1,21 @@ +import { MaybePromise } from "../../type.ts"; + +export interface VioObjectCreateDto { + name?: string; + id: number; + type: string; +} +export interface VioObjectDto { + name?: string; + id: number; + type: string; +} +export interface ServerObjectBaseExposed { + getObjects(filter?: { name?: string; type?: string }): MaybePromise<{ list: VioObjectCreateDto[] }>; +} +export interface ClientObjectBaseExposed { + /** 删除指定 UI 对象 */ + deleteObject(objectId: number): void; + /** 创建 UI 对象 */ + createObject(info: VioObjectCreateDto): void; +} diff --git a/vio/src/vio/vio_object/_object_base.type.ts b/vio/src/vio/vio_object/_object_base.type.ts new file mode 100644 index 0000000..d629696 --- /dev/null +++ b/vio/src/vio/vio_object/_object_base.type.ts @@ -0,0 +1,6 @@ +/** @public */ +export interface VioObject { + name?: string; + readonly id: number; + readonly type: string; +} diff --git a/vio/src/vio/vio_object/_server_exposed.ts b/vio/src/vio/vio_object/_server_exposed.ts new file mode 100644 index 0000000..918530b --- /dev/null +++ b/vio/src/vio/vio_object/_server_exposed.ts @@ -0,0 +1,91 @@ +import { indexRecordToArray } from "../../lib/array_like.ts"; +import { MaybePromise } from "../../type.ts"; +import { VioChart as RpcVioChart } from "./chart/VioChart.ts"; +import { VioTableImpl } from "./table/VioTable.ts"; +import { ServerObjectExposed, TableDataDto, VioObjectDto, VioTableDto } from "./object.dto.ts"; +import { TableFilter, TableRow, ChartDataItem, ChartInfo, RequestUpdateRes, VioChart, Key } from "./object.type.ts"; + +import { VioObjectCenterImpl } from "./_VioObjectCenter.ts"; + +export class RpcServerObjectExposed implements ServerObjectExposed { + constructor(objectCenter: VioObjectCenterImpl) { + this.#center = objectCenter; + } + #center: VioObjectCenterImpl; + + getObjects(): { list: VioObjectDto[] } { + const list = new Array(this.#center.chartsNumber); + let i = 0; + for (const item of this.#center.getAll()) { + list[i++] = { id: item.id, type: item.type, name: item.name }; + } + return { list }; + } + + /* Chart */ + + #getChart(id: number): RpcVioChart { + const chart = this.#center.getObject(id); + if (chart instanceof RpcVioChart) return chart; + throw new Error(`Chart ${id} does not exist`); + } + getChartInfo(id: number): ChartInfo { + const chart = this.#getChart(id); + return getChartInfo(chart, id); + } + requestUpdateChart(chartId: number): MaybePromise> { + const chart = this.#getChart(chartId); + return chart.requestUpdate() as MaybePromise>; + } + + /* Table */ + #getTable(id: number): VioTableImpl { + const object = this.#center.getObject(id); + if (object instanceof VioTableImpl) return object; + throw new Error(`Table ${id} does not exist`); + } + getTable(id: number): VioTableDto { + const table = this.#getTable(id); + return { columns: table.columns, ...table.config, id: table.id }; + } + getTableData(tableId: number, filter?: TableFilter): TableDataDto { + const table = this.#getTable(tableId); + return table.getRows(filter); + } + onTableAction(tableId: number, operateKey: string, rowKeys: string[]): void { + this.#getTable(tableId).onTableAction(operateKey, rowKeys); + } + onTableRowAction(tableId: number, operateKey: string, rowKey: Key): void { + isKey(rowKey); + this.#getTable(tableId).onRowAction(operateKey, rowKey); + } + onTableRowAdd(tableId: number, param: TableRow): void { + this.#getTable(tableId).onRowAdd(param); + } + onTableRowUpdate(tableId: number, rowKey: string, param: TableRow): void { + isKey(rowKey); + this.#getTable(tableId).onRowUpdate(rowKey, param); + } +} +function isKey(key: Key) { + let type = typeof key; + if (type === "string" || type == "number") return; + throw new Error("rowKey must be a string or number:" + typeof key); +} +function getChartInfo(chart: VioChart, id: number): ChartInfo { + const cacheList: ChartDataItem[] = new Array(chart.cachedSize); + let i = 0; + for (const item of chart.getCacheDateItem()) { + let dataItem: ChartDataItem = { data: item.data, timestamp: item.timestamp }; + if (item.timeName) dataItem.timeName = item.timeName; + cacheList[i++] = dataItem; + } + return { + name: chart.name, + meta: chart.meta, + dimension: chart.dimension, + id, + cacheList, + dimensions: indexRecordToArray(chart.dimensions), + }; +} diff --git a/vio/src/vio/vio_object/_ui/_base.ts b/vio/src/vio/vio_object/_ui/_base.ts new file mode 100644 index 0000000..90967e2 --- /dev/null +++ b/vio/src/vio/vio_object/_ui/_base.ts @@ -0,0 +1,40 @@ +/** @public */ +export interface UiBase { + ui: string; +} + +/** @public */ +export interface UiInput extends UiBase {} + +/** @public */ +export interface UiOutput extends UiBase {} + +/** @public */ +export interface UiAction extends UiBase { + key: string; + // onTrigger?(): void; +} + +const INPUT = new Set(); +export function addUiInput(key: string) { + INPUT.add(key); +} +export function isUiInput(uiObject: UiBase): UiInput | undefined { + if (INPUT.has(uiObject.ui)) return uiObject as UiInput; +} + +const OUTPUT = new Set(); +export function addUiOutput(key: string) { + OUTPUT.add(key); +} +export function isUiOutput(uiObject: UiBase): UiOutput | undefined { + if (OUTPUT.has(uiObject.ui)) return uiObject as UiOutput; +} + +const ACTION = new Set(); +export function addUiAction(key: string) { + ACTION.add(key); +} +export function isUiAction(uiObject: UiBase): UiAction | undefined { + if (ACTION.has(uiObject.ui)) return uiObject as UiAction; +} diff --git a/vio/src/vio/vio_object/_ui/_define.ts b/vio/src/vio/vio_object/_ui/_define.ts new file mode 100644 index 0000000..bc38b0b --- /dev/null +++ b/vio/src/vio/vio_object/_ui/_define.ts @@ -0,0 +1,31 @@ +import { addUiAction, addUiInput, addUiOutput, UiAction, UiOutput } from "./_base.ts"; +/** @public */ +export class UiButton implements UiAction { + constructor( + readonly key: string, + props?: UiButton["props"], + ) { + this.props = props; + } + readonly ui = "button"; + props?: { + icon?: string; + text?: string; + type?: string; + tooltip?: string; + + disable?: boolean; + }; +} +addUiAction("button"); + +/** @public */ +export type UiTag = UiOutput & { + ui: "tag"; + props: { + text?: string; + icon?: string; + color?: string; + }; +}; +addUiOutput("tag"); diff --git a/vio/src/vio/vio_object/_ui/mod.private.ts b/vio/src/vio/vio_object/_ui/mod.private.ts new file mode 100644 index 0000000..b94a4fd --- /dev/null +++ b/vio/src/vio/vio_object/_ui/mod.private.ts @@ -0,0 +1,2 @@ +export * from "./_base.ts"; +export * from "./_define.ts"; diff --git a/vio/src/vio/vio_object/_ui/mod.ts b/vio/src/vio/vio_object/_ui/mod.ts new file mode 100644 index 0000000..4d436d8 --- /dev/null +++ b/vio/src/vio/vio_object/_ui/mod.ts @@ -0,0 +1,2 @@ +export type { UiAction, UiBase, UiInput, UiOutput } from "./_base.ts"; +export * from "./_define.ts"; diff --git a/vio/src/vio/vio_object/chart/VioChart.ts b/vio/src/vio/vio_object/chart/VioChart.ts new file mode 100644 index 0000000..9f5641e --- /dev/null +++ b/vio/src/vio/vio_object/chart/VioChart.ts @@ -0,0 +1,68 @@ +import { deepClone } from "evlib"; +import { + ChartCreateOption, + ChartUpdateLowerOption, + ChartUpdateOption, + DimensionalityReduction, + IntersectingDimension, +} from "./chart.type.ts"; +import { VioChartBase } from "./VioChartBase.ts"; +import { ChartUpdateData, ChartUpdateSubData, ClientChartExposed } from "./chart.dto.ts"; +import { ClientObjectBaseExposed } from "../_object_base.dto.ts"; + +export class VioChart extends VioChartBase { + constructor( + ctrl: ClientChartExposed & ClientObjectBaseExposed, + chartId: number, + dimension: number, + options: ChartCreateOption = {}, + ) { + super({ + ...options, + id: chartId, + dimension, + }); + this.#ctrl = ctrl; + } + + #ctrl?: ClientChartExposed & ClientObjectBaseExposed; + /** @override */ + updateData(data: T, timeName?: string): void { + if (!this.#ctrl) return; + let internalData = typeof data === "object" ? deepClone(data) : data; + + const timestamp = Date.now(); + this.pushCache({ data: internalData, timestamp, timeName }); + + const writeData: ChartUpdateData = { data: internalData, timestamp: timestamp, timeAxisName: timeName }; + this.#ctrl.writeChart(this.id, writeData); + } + updateSubData(updateData: DimensionalityReduction, coord: number, opts?: ChartUpdateLowerOption): void; + updateSubData(updateData: IntersectingDimension, coord: (number | undefined)[], opts?: ChartUpdateOption): void; + /** @override */ + updateSubData( + updateData: IntersectingDimension, + coord: number | (number | undefined)[], + opts?: ChartUpdateLowerOption | ChartUpdateOption, + ): void { + if (!this.#ctrl) return; + const timestamp = Date.now(); + this.#ctrl.writeChart(this.id, { + data: updateData, + coord: coord as any, + timeAxisName: opts?.timeName, + timestamp: timestamp, + } satisfies ChartUpdateSubData); + if (this.maxCacheSize <= 0) return; + //@ts-ignore + super.updateSubData(updateData, coord, opts); + } + get disposed() { + return !this.#ctrl; + } + dispose() { + if (!this.#ctrl) return; + this.#ctrl.deleteObject(this.id); + this.#ctrl = undefined; + } +} diff --git a/vio/src/vio/classes/VioChart.ts b/vio/src/vio/vio_object/chart/VioChartBase.ts similarity index 65% rename from vio/src/vio/classes/VioChart.ts rename to vio/src/vio/vio_object/chart/VioChartBase.ts index b8ca1eb..ac8057d 100644 --- a/vio/src/vio/classes/VioChart.ts +++ b/vio/src/vio/vio_object/chart/VioChartBase.ts @@ -1,22 +1,27 @@ import type { ChartDataItem, + ChartUpdateLowerOption, DimensionInfo, DimensionalityReduction, IntersectingDimension, + RequestUpdateRes, + VioChart, + VioChartCreateConfig, VioChartMeta, -} from "../api_type.ts"; +} from "./chart.type.ts"; import { LinkedCacheQueue } from "evlib/data_struct"; import { deepClone } from "evlib"; -import { IndexRecord } from "../../lib/array_like.ts"; -import { MaybePromise } from "../../type.ts"; +import { IndexRecord } from "../../../lib/array_like.ts"; +import { MaybePromise } from "../../../type.ts"; -export abstract class VioChartImpl implements VioChart { +export abstract class VioChartBase implements VioChart { constructor(config: VioChartCreateConfig) { const { dimensions, dimension, maxCacheSize = 0, meta, onRequestUpdate, updateThrottle = 0 } = config; if (!(dimension >= 0)) throw new RangeError("dimension must be a positive integer"); this.#cache = new LinkedCacheQueue>(maxCacheSize); this.dimension = dimension; this.id = config.id; + this.name = config.name; const finalDimension: IndexRecord = { length: dimension }; Object.defineProperty(finalDimension, "length", { @@ -38,12 +43,14 @@ export abstract class VioChartImpl implements VioChart { this.dimensions = finalDimension; this.meta = { ...meta }; this.updateThrottle = updateThrottle; - this.onRequestUpdate = onRequestUpdate; + this.#onRequestUpdate = onRequestUpdate; } + readonly type = "chart"; updateThrottle: number; - onRequestUpdate?: () => MaybePromise; - + /** 主动请求更新的回调函数 */ + #onRequestUpdate?: () => MaybePromise; + name?: string; #cache: LinkedCacheQueue>; /** 已缓存的数据长度 */ get cachedSize(): number { @@ -112,75 +119,37 @@ export abstract class VioChartImpl implements VioChart { readonly dimension: number; readonly id: number; readonly meta: VioChartMeta; -} -/** - * VIO Chart - * @public - * @category Chart - */ -export interface VioChart { - data?: T; - cachedSize: number; - maxCacheSize: number; - /** 请求更新节流。单位毫秒 */ - updateThrottle: number; - /** 主动请求更新的回调函数 */ - onRequestUpdate?: () => MaybePromise; - getCacheDateItem(): IterableIterator>>; - /** 获取缓存中的数据 */ - getCacheData(): IterableIterator; - /** 更新图表数据。并将数据推入缓存 */ - updateData(data: T, timeName?: string): void; - /** 维度信息 */ - readonly dimensions: ArrayLike; - /** 维度数量 */ - readonly dimension: number; - readonly id: number; - readonly meta: VioChartMeta; -} + #lastData?: MaybePromise>; + requestUpdate(): MaybePromise> { + if (!this.#onRequestUpdate) throw new Error("Requests for updates are not allowed"); + const timestamp = Date.now(); -/** - * VioChart 创建配置 - * @public - * @category Chart - */ -export type VioChartCreateConfig = ChartCreateOption & { - /** {@inheritdoc VioChart.id} */ - id: number; - /** {@inheritdoc VioChart.dimension} */ - dimension: number; - /** {@inheritdoc VioChart.onRequestUpdate} */ - onRequestUpdate?(): MaybePromise; - /** {@inheritdoc VioChart.updateThrottle} */ - updateThrottle?: number; -}; -/** - * VioChart 创建可选项 - * @public - * @category Chart - */ -export type ChartCreateOption = { - /** {@inheritdoc VioChart.meta} */ - meta?: VioChartMeta; - /** {@inheritdoc VioChart.dimensions} */ - dimensions?: Record; - /** {@inheritdoc VioChart.maxCacheSize} */ - maxCacheSize?: number; -}; -/** - * @public - * @category Chart - */ -export type ChartUpdateOption = { - /** 时间轴刻度名称 */ - timeName?: string; -}; -/** - * @public - * @category Chart - */ -export type ChartUpdateLowerOption = { - /** {@inheritdoc ChartUpdateOption.timeName} */ - timeName?: string; -}; + if (this.#lastData) { + if (this.#lastData instanceof Promise) return this.#lastData; + if (timestamp - this.#lastData.timestamp <= this.updateThrottle) return this.#lastData; + } + + const value = this.#onRequestUpdate(); + let res: MaybePromise>; + if (value instanceof Promise) + res = value.then( + (value): Readonly> => { + let res = { data: value, timestamp: timestamp } as const; + this.pushCache({ data: value, timestamp }); + this.#lastData = res; + return res; + }, + (e) => { + this.#lastData = undefined; + throw e; + }, + ); + else { + res = { data: value, timestamp: timestamp }; + this.pushCache({ data: value, timestamp }); + } + this.#lastData = res; + return res; + } +} diff --git a/vio/src/vio/vio_object/chart/chart.dto.ts b/vio/src/vio/vio_object/chart/chart.dto.ts new file mode 100644 index 0000000..cc71652 --- /dev/null +++ b/vio/src/vio/vio_object/chart/chart.dto.ts @@ -0,0 +1,31 @@ +import { MaybePromise } from "../../../type.ts"; +import { ChartInfo, DimensionalityReduction, IntersectingDimension, RequestUpdateRes } from "./chart.type.ts"; + +type ChartUpdateCommonData = { + /** 时间刻度名称 */ + timeAxisName?: string; + /** 时间刻度(时间戳) */ + timestamp: number; +}; +export type ChartUpdateData = ChartUpdateCommonData & { + coord?: undefined; + data: T; +}; +export type ChartUpdateSubData = + | ChartUpdateCommonData + | { + coord: number | (number | undefined)[]; + data: IntersectingDimension; + } + | { coord: number; data: DimensionalityReduction }; + +export interface ClientChartExposed { + /** 在指定图表输出数据 */ + writeChart(chartId: number, data: Readonly>): void; +} +export interface ServerChartExposed { + /** 获取指定图表的信息 */ + getChartInfo(chartId: number): MaybePromise; + /** 主动请求更新图 */ + requestUpdateChart(chartId: number): MaybePromise>; +} diff --git a/vio/src/vio/api_type/chart.type.ts b/vio/src/vio/vio_object/chart/chart.type.ts similarity index 54% rename from vio/src/vio/api_type/chart.type.ts rename to vio/src/vio/vio_object/chart/chart.type.ts index f844c5c..3dc03a4 100644 --- a/vio/src/vio/api_type/chart.type.ts +++ b/vio/src/vio/vio_object/chart/chart.type.ts @@ -1,3 +1,6 @@ +import { MaybePromise } from "../../../type.ts"; +import { VioObject } from "../_object_base.type.ts"; + /** * 一个图表的信息 * @public @@ -5,6 +8,7 @@ */ export interface ChartInfo { id: number; + name?: string; cacheList: ChartDataItem[]; /** 维度数量。不包含时间维度 */ dimension: number; @@ -25,28 +29,6 @@ export interface DimensionInfo { indexNames?: string[]; } -/** @public */ -export type ChartCreateInfo = Pick & { - meta?: VioChartMeta; -}; -type ChartUpdateCommonData = { - /** 时间刻度名称 */ - timeAxisName?: string; - /** 时间刻度(时间戳) */ - timestamp: number; -}; -export type ChartUpdateData = ChartUpdateCommonData & { - coord?: undefined; - data: T; -}; -export type ChartUpdateSubData = - | ChartUpdateCommonData - | { - coord: number | (number | undefined)[]; - data: IntersectingDimension; - } - | { coord: number; data: DimensionalityReduction }; - /** @public */ export type IntersectingDimension = T extends Array ? P | IntersectingDimension

: never; @@ -133,3 +115,77 @@ export namespace ChartMeta { chartType: "pie"; } } + +/** + * VIO Chart + * @public + * @category Chart + */ +export interface VioChart extends VioObject { + data?: T; + cachedSize: number; + maxCacheSize: number; + + /** 请求更新节流。单位毫秒 */ + updateThrottle: number; + + /** web 端主动请求更新图表,这会触发 chart.onRequestUpdate() */ + requestUpdate(): MaybePromise>; + getCacheDateItem(): IterableIterator>>; + /** 获取缓存中的数据 */ + getCacheData(): IterableIterator; + /** 更新图表数据。并将数据推入缓存 */ + updateData(data: T, timeName?: string): void; + /** 维度信息 */ + readonly dimensions: ArrayLike; + /** 维度数量 */ + readonly dimension: number; + readonly meta: VioChartMeta; + readonly type: "chart"; +} + +/** + * VioChart 创建配置 + * @public + * @category Chart + */ +export type VioChartCreateConfig = ChartCreateOption & { + /** {@inheritdoc VioObject.id} */ + id: number; + /** {@inheritdoc VioChart.dimension} */ + dimension: number; +}; +/** + * VioChart 创建可选项 + * @public + * @category Chart + */ +export type ChartCreateOption = { + name?: string; + /** {@inheritdoc VioChart.meta} */ + meta?: VioChartMeta; + /** {@inheritdoc VioChart.dimensions} */ + dimensions?: Record; + /** {@inheritdoc VioChart.maxCacheSize} */ + maxCacheSize?: number; + /** {@inheritdoc VioChart.updateThrottle} */ + updateThrottle?: number; + /** 请求更新图的数据。 */ + onRequestUpdate?(): MaybePromise; +}; +/** + * @public + * @category Chart + */ +export type ChartUpdateOption = { + /** 时间轴刻度名称 */ + timeName?: string; +}; +/** + * @public + * @category Chart + */ +export type ChartUpdateLowerOption = { + /** {@inheritdoc ChartUpdateOption.timeName} */ + timeName?: string; +}; diff --git a/vio/src/vio/vio_object/mod.private.ts b/vio/src/vio/vio_object/mod.private.ts new file mode 100644 index 0000000..7304deb --- /dev/null +++ b/vio/src/vio/vio_object/mod.private.ts @@ -0,0 +1,7 @@ +export * from "./_VioObjectCenter.ts"; +export * from "./object.type.ts"; +export * from "./_server_exposed.ts"; +export * from "./_ui/mod.private.ts"; + +export * from "./chart/VioChartBase.ts"; +export * from "./table/TableBase.ts"; diff --git a/vio/src/vio/vio_object/mod.ts b/vio/src/vio/vio_object/mod.ts new file mode 100644 index 0000000..9b7b7a2 --- /dev/null +++ b/vio/src/vio/vio_object/mod.ts @@ -0,0 +1,2 @@ +export * from "./object.type.ts"; +export * from "./_ui/mod.ts"; diff --git a/vio/src/vio/vio_object/object.dto.ts b/vio/src/vio/vio_object/object.dto.ts new file mode 100644 index 0000000..99fef01 --- /dev/null +++ b/vio/src/vio/vio_object/object.dto.ts @@ -0,0 +1,10 @@ +import type { ClientObjectBaseExposed, ServerObjectBaseExposed } from "./_object_base.dto.ts"; +import type { ClientTableExposed, ServerTableExposed } from "./table/table.dto.ts"; +import type { ClientChartExposed, ServerChartExposed } from "./chart/chart.dto.ts"; + +export * from "./_object_base.dto.ts"; +export * from "./chart/chart.dto.ts"; +export * from "./table/table.dto.ts"; + +export type ServerObjectExposed = ServerObjectBaseExposed & ServerChartExposed & ServerTableExposed; +export type ClientObjectExposed = ClientObjectBaseExposed & ClientChartExposed & ClientTableExposed; diff --git a/vio/src/vio/vio_object/object.type.ts b/vio/src/vio/vio_object/object.type.ts new file mode 100644 index 0000000..cc9fabb --- /dev/null +++ b/vio/src/vio/vio_object/object.type.ts @@ -0,0 +1,51 @@ +import type { ChartCreateOption, VioChart } from "./chart/chart.type.ts"; +import type { VioObject } from "./_object_base.type.ts"; +import { TableRow, VioTable, Column, TableCreateOption } from "./table/table.type.ts"; + +export * from "./chart/chart.type.ts"; +export * from "./table/table.type.ts"; +export * from "./_object_base.type.ts"; +/** + * @public + * @category Chart + */ +export interface VioObjectCenter { + disposeObject(object: VioObject): void; + chartsNumber: number; + + /** 获取所有已 Vio 对象*/ + getAll(): IterableIterator; +} +/** @public */ +export interface VioObjectCenter { + /** 创建一维图表 */ + createChart(dimension: 1, options?: ChartCreateOption): VioChart; + /** 创建二维图表 */ + createChart(dimension: 2, options?: ChartCreateOption): VioChart; + /** 创建三维图表 */ + createChart(dimension: 3, options?: ChartCreateOption): VioChart; + createChart(dimension: number, options?: ChartCreateOption): VioChart; + + /** + * 创建一维图表 + * @deprecated 改用 createChart + */ + create(dimension: 1, options?: ChartCreateOption): VioChart; + /** 创建二维图表 + * @deprecated 改用 createChart */ + create(dimension: 2, options?: ChartCreateOption): VioChart; + /** 创建三维图表 + * @deprecated 改用 createChart */ + create(dimension: 3, options?: ChartCreateOption): VioChart; + /** @deprecated 改用 createChart */ + create(dimension: number, options?: ChartCreateOption): VioChart; + /** @deprecated 改用 disposeObject */ + disposeChart(chart: VioChart): void; + /** @deprecated 已废弃 */ + get(chartId: number): VioChart | undefined; +} + +/** @public */ +export interface VioObjectCenter { + createTable(columns: Column[], option?: TableCreateOption): VioTable; +} diff --git a/vio/src/vio/vio_object/table/TableBase.ts b/vio/src/vio/vio_object/table/TableBase.ts new file mode 100644 index 0000000..86a1922 --- /dev/null +++ b/vio/src/vio/vio_object/table/TableBase.ts @@ -0,0 +1,87 @@ +import { TableCreateOption, Column, TableRow, TableFilter, Key } from "./table.type.ts"; +import { TableDataDto } from "./table.dto.ts"; +import { removeUndefinedKey } from "evlib"; + +export class VioTableBase { + constructor(id: number, columns: Readonly>[], option: TableCreateOption) { + this.id = id; + if (typeof option.keyField !== "string") throw new Error("option.keyField must be a string"); + this.config = { + keyField: option.keyField, + addAction: option.addAction, + name: option.name, + operations: option.operations, + updateAction: option.updateAction, + }; + removeUndefinedKey(this.config); + + this.columns = columns.map((item): Column => { + const columns: Column = { + dataIndex: item.dataIndex as string, + title: item.title, + width: item.width, + render: item.render, + }; + removeUndefinedKey(columns); + return columns; + }); + } + readonly config: Readonly; + readonly columns: Readonly[]; + readonly type = "table"; + id: number; + get name() { + return this.config.name; + } + get rowNumber() { + return this.#data.length; + } + #data: Row[] = []; + get data(): Row[] { + return this.#data; + } + + getRow(index: number): Row { + return this.data[index]; + } + getRowIndexByKey(key: Key) { + const keyField = this.config.keyField; + return this.data.findIndex((item) => item[keyField] === key); + } + updateRow(row: Row, index: number): void { + const oldRow = this.#data[index]; + if (!oldRow) throw new RangeError(`Index '${index}' exceeds the range of the table`); + this.#data[index] = row; + } + updateTable(data: Row[]): void { + this.#data = data; + } + addRow(row: Row, index?: number): void { + const arr = this.#data; + if (index === undefined || index === arr.length) { + arr.push(row); + } else { + if (index < 0 || index > arr.length) throw new RangeError(`invalid index '${index}'`); + let i = arr.length; + while (i > index) { + arr[i] = arr[--i]; + } + arr[i] = row; + } + } + deleteRow(index: number, count = 1): Row[] { + return this.#data.splice(index, count); + } + getRows(filter: TableFilter = {}): TableDataDto { + let { skip = 0, number = Infinity } = filter; + const res: Row[] = []; + const index: number[] = []; + const arr = this.#data; + while (skip < arr.length && res.length < number) { + let idx = skip++; + res.push(arr[idx]); + index.push(idx); + } + return { rows: res, index, total: this.rowNumber }; + } +} diff --git a/vio/src/vio/vio_object/table/VioTable.ts b/vio/src/vio/vio_object/table/VioTable.ts new file mode 100644 index 0000000..77006c3 --- /dev/null +++ b/vio/src/vio/vio_object/table/VioTable.ts @@ -0,0 +1,105 @@ +import { ClientTableExposed, TableChanges } from "./table.dto.ts"; +import { VioTable, TableCreateOption, Column, TableRow, Key } from "./table.type.ts"; +import { VioTableBase } from "./TableBase.ts"; + +export class VioTableImpl + extends VioTableBase + implements VioTable +{ + constructor(ctrl: ClientTableExposed, id: number, columns: Readonly>[], option: TableCreateOption) { + super(id, columns, option); + this.#ctrl = ctrl; + } + #ctrl: ClientTableExposed; + updateRow(row: Row, index: number): void { + const oldRow = this.data[index]; + super.updateRow(row, index); + const keyField = this.config.keyField; + + const key = oldRow[keyField]; + const newKey = row[keyField]; + + if (key === newKey) { + if (this.#addMerge.has(key)) this.#addMerge.set(key, row); + else this.#updateMerge.set(key, row); + } else { + if (this.#addMerge.has(key)) { + this.#addMerge.delete(key); + this.#addMerge.set(newKey, row); + } else { + this.#deleteMerge.add(key); + this.#addMerge.set(newKey, row); + } + } + this.#check(); + } + addRow(row: Row, index: number = this.data.length): void { + super.addRow(row, index); + const key = row[this.config.keyField]; + if (this.#deleteMerge.has(key)) this.#deleteMerge.delete(key); + this.#addMerge.set(key, row); + this.#check(); + } + deleteRow(index: number, count = 1): Row[] { + const deleted = super.deleteRow(index, count); + const keyField = this.config.keyField; + + for (const item of deleted) { + let key = item[keyField]; + if (this.#addMerge.has(key)) this.#addMerge.delete(key); + else { + if (this.#updateMerge.has(key)) this.#updateMerge.delete(key); + this.#deleteMerge.add(key); + } + } + this.#check(); + return deleted; + } + updateTable(data: Row[]): void { + super.updateTable(data); + this.#skipUpdate(); + this.#ctrl.updateTable(this.id); + } + + onTableAction(operateKey: string, rowKeys: Key[]): void {} + onRowAdd(param: Add): void {} + onRowAction(operateKey: string, rowKey: Key): void {} + onRowUpdate(rowKey: string, param: Update): void {} + + /* 合并发送 */ + + #deleteMerge: Set = new Set(); + #addMerge = new Map(); + #updateMerge = new Map(); + #timer?: number; + #skipUpdate() { + clearTimeout(this.#timer); + this.#timer = undefined; + this.#deleteMerge.clear(); + this.#addMerge.clear(); + this.#updateMerge.clear(); + } + #check() { + if (this.#timer === undefined) { + this.#timer = setTimeout(this.#send, 33) as any; + } + } + #send = () => { + const changes: TableChanges = {}; + if (this.#addMerge.size) { + changes.add = Array.from(this.#addMerge.values()); + this.#addMerge.clear(); + } + if (this.#deleteMerge.size) { + changes.delete = Array.from(this.#deleteMerge); + this.#deleteMerge.clear(); + } + + if (this.#updateMerge.size) { + changes.update = Array.from(this.#updateMerge.values()); + this.#updateMerge.clear(); + } + this.#ctrl.tableChange(this.id, changes); + this.#timer = undefined; + }; +} diff --git a/vio/src/vio/vio_object/table/table.dto.ts b/vio/src/vio/vio_object/table/table.dto.ts new file mode 100644 index 0000000..a07ce01 --- /dev/null +++ b/vio/src/vio/vio_object/table/table.dto.ts @@ -0,0 +1,55 @@ +import { MaybePromise } from "../../../type.ts"; +import { Column, Key, TableFilter, TableRow } from "./table.type.ts"; +import { UiAction, UiButton } from "../_ui/mod.ts"; + +export type VioTableDto = { + id: number; + name?: string; + + keyField: string; + columns: ColumnDto[]; + operations?: UiAction[]; + rowOperation?: UiAction[]; + updateAction?: boolean; + addAction?: UiButton["props"]; +}; +export type TableDataDto = { + rows: Row[]; + index: number[]; + total: number; +}; +export type ColumnDto = Pick & {}; + +export interface ViewTable extends ClientTableExposed { + rows: Row[]; + pageIndex: number; + pageSize: number; + loading: boolean; + readonly columns: Column; + + /** 行数量 */ + rowNumber: number; +} +export interface ServerTableExposed< + Row extends TableRow = TableRow, + Add extends TableRow = Row, + Update extends TableRow = Add, +> { + /** 表格操作事件 */ + onTableAction(tableId: number, operateKey: string, rowKeys: Key[]): void; + /** 行事件 */ + onTableRowAction(tableId: number, operateKey: string, rowKey: Key): void; + /** 添加行事件 */ + onTableRowAdd(tableId: number, param: Add): void; + /** 更新行事件 */ + onTableRowUpdate(tableId: number, rowKey: Key, param: Update): void; + + getTableData(tableId: number, filter?: TableFilter): MaybePromise>; + getTable(id: number): MaybePromise; +} + +export interface ClientTableExposed { + tableChange(tableId: number, changes: TableChanges): void; + updateTable(tableId: number): void; +} +export type TableChanges = { update?: Row[]; delete?: Key[]; add?: Row[] }; diff --git a/vio/src/vio/vio_object/table/table.type.ts b/vio/src/vio/vio_object/table/table.type.ts new file mode 100644 index 0000000..f642e31 --- /dev/null +++ b/vio/src/vio/vio_object/table/table.type.ts @@ -0,0 +1,66 @@ +import { VioObject } from "../_object_base.type.ts"; +import { UiAction, UiButton, UiBase } from "../_ui/mod.ts"; + +/** @public */ +export type TableRow = { [key: string]: any }; + +/** @public */ +export interface VioTable + extends VioObject { + /** 表格操作事件 */ + onTableAction(operateKey: string, rowKeys: Key[]): void; + /** 行事件 */ + onRowAction(operateKey: string, rowKey: Key): void; + /** 添加行事件 */ + onRowAdd(param: Add): void; + /** 更新行事件 */ + onRowUpdate(rowKey: string, param: Update): void; + + /** 行数量 */ + rowNumber: number; + readonly type: "table"; + getRowIndexByKey(key: Key): number; + getRow(index: number): Row; + /** 更新表格数据 */ + updateTable(data: Row[]): void; + updateRow(row: Row, index: number): void; + /** 添加表格行 */ + addRow(row: Row, afterIndex?: number): void; + /** 删除表格行 */ + deleteRow(index: number, count: number): void; + + getRows(filter?: TableFilter): { rows: Row[]; index: number[] }; +} +/** @public */ +export type TableFilter = { + skip?: number; + number?: number; +}; + +/** @public */ +export type TableCreateOption = { + keyField: string; + name?: string; + /** 表格操作 */ + operations?: UiAction[]; + /** 更新操作 */ + updateAction?: boolean; + /** 新增操作 */ + addAction?: UiButton["props"]; +}; +/** @public */ +export type Column = { + title?: string; + dataIndex?: keyof Row; + width?: number; + render?: string; +}; +/** @public */ +export type TableRenderFn = (args: { + record: Row; + index: number; + column: Readonly>; +}) => UiBase | UiBase[]; + +/** @public */ +export type Key = string | number; diff --git a/vio/test/_env/test_port.ts b/vio/test/_env/test_port.ts index b4d4da7..ce726a8 100644 --- a/vio/test/_env/test_port.ts +++ b/vio/test/_env/test_port.ts @@ -50,7 +50,7 @@ export async function connectVioServer(host: string) { const cpc = await connectRpc(host); const clientApi = createMockClientApi(); - cpc.setObject(clientApi); + cpc.exposeObject(clientApi); const serverApi: MakeCallers = cpc.genCaller(); cpc.onClose.catch(() => {}); @@ -60,11 +60,19 @@ export async function connectVioServer(host: string) { export function createMockClientApi() { return { - createChart: vi.fn(), - deleteChart: vi.fn(), - sendTtyReadRequest: vi.fn(), - writeChart: vi.fn(), - writeTty: vi.fn(), - ttyReadEnableChange: vi.fn(), + object: { + createObject: vi.fn(), + deleteObject: vi.fn(), + + writeChart: vi.fn(), + + tableChange: vi.fn(), + updateTable: vi.fn(), + }, + tty: { + sendTtyReadRequest: vi.fn(), + writeTty: vi.fn(), + ttyReadEnableChange: vi.fn(), + }, } satisfies VioClientExposed; } diff --git a/vio/test/chart/chart_api.test.ts b/vio/test/chart/chart_api.test.ts deleted file mode 100644 index c70c2e0..0000000 --- a/vio/test/chart/chart_api.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { describe, expect, vi } from "vitest"; -import { connectWebsocket } from "../../src/lib/websocket.ts"; -import { CenterCreateChartOption } from "@asla/vio"; -import { ChartInfo, ChartCreateInfo, DimensionInfo } from "../../src/client.ts"; -import { createWebSocketCpc } from "cpcall"; -import { afterTime } from "evlib"; -import { vioServerTest as test } from "../_env/test_port.ts"; - -test("create", async function ({ vio, connector }) { - const { clientApi, serverApi } = connector; - await expect(serverApi.getCharts()).resolves.toEqual({ list: [] }); - - const config: CenterCreateChartOption = { meta: { chartType: "progress" }, maxCacheSize: 20 }; - const chart = vio.chart.create(1, config); //创建 - expect(chart).toMatchObject(config); - - let allCharts = await serverApi.getCharts(); - expect(allCharts).toEqual({ - list: [ - { - id: chart.id, - dimension: chart.dimension, - meta: config.meta!, - cacheList: [], - dimensions: [{}], - }, - ], - } satisfies typeof allCharts); - - expect(clientApi.createChart).toBeCalledWith({ - meta: config.meta!, - dimension: chart.dimension, - id: chart.id, - dimensions: [{}], - } satisfies ChartCreateInfo); -}); -test("dimensionsInfo", async function ({ vio }) { - const chart = vio.chart.create(2, { dimensions: { 1: { name: "Y" } } }); //创建 - expect(chart.dimensions.length).toBe(2); - expect(chart.dimensions[1]).toMatchObject({ name: "Y" } satisfies DimensionInfo); -}); -test("dispose chart", async function ({ vio, connector }) { - const { clientApi, serverApi } = connector; - const chart0 = vio.chart.create(0); - const chart1 = vio.chart.create(2); - await expect(serverApi.getCharts().then((res) => res.list)).resolves.toHaveLength(2); - let chart3 = vio.chart.create(3); - await expect(serverApi.getCharts().then((res) => res.list)).resolves.toHaveLength(3); - - vio.chart.disposeChart(chart1); - - await expect(serverApi.getCharts().then((res) => res.list.map((info) => info.id))).resolves.toEqual([0, 2]); - - expect(clientApi.deleteChart).toBeCalledWith(chart1.id); -}); -test("update", async function ({ vio, connector }) { - const { clientApi, serverApi } = connector; - - const config: CenterCreateChartOption = { maxCacheSize: 20 }; - const chart = vio.chart.create(1, config); //创建 - - const find = ({ list }: { list: ChartInfo[] }) => list.find((item) => item.id === chart.id); - - await expect(serverApi.getCharts().then(find)).resolves.toMatchObject({ cacheList: [] } satisfies Partial); - chart.updateData(1); //更新 - chart.updateData(2); //更新 - chart.updateData(3); //更新 - - expect(Array.from(chart.getCacheData())).toEqual([1, 2, 3]); - - await expect(serverApi.getCharts().then((res) => find(res)?.cacheList.map((item) => item.data))).resolves.toEqual([ - 1, 2, 3, - ]); - - expect(clientApi.writeChart).toBeCalledTimes(3); - - expect(clientApi.writeChart.mock.calls.map((item) => item[1])).toMatchObject([{ data: 1 }, { data: 2 }, { data: 3 }]); - - vio.chart.disposeChart(chart); -}); -test("cache", async function ({ vio }) { - const config: CenterCreateChartOption = { maxCacheSize: 4 }; - const chart = vio.chart.create(1, config); //创建 - - for (let i = 0; i < chart.maxCacheSize; i++) { - chart.updateData(i); - } - expect(Array.from(chart.getCacheData())).toEqual([0, 1, 2, 3]); - chart.updateData(4); - expect(Array.from(chart.getCacheData())).toEqual([1, 2, 3, 4]); -}); -test("Proactive update", async function ({ vio, connector }) { - const { clientApi, serverApi } = connector; - const chart1 = vio.chart.create(1); - - let i = 0; - const onRequestUpdate = vi.fn(() => i++); - const chart2 = vio.chart.create(2, { onRequestUpdate, updateThrottle: 20 }); - - await expect(serverApi.requestUpdateChart(chart1.id), "Chart1 没有设置更新函数,应抛出异常").rejects.toThrowError(); - await expect(serverApi.requestUpdateChart(chart2.id)).resolves.toMatchObject({ data: 0 }); - await expect( - serverApi.requestUpdateChart(chart2.id), - "请求频率超过设定的节流时间,返回的值还是原来的", - ).resolves.toMatchObject({ data: 0 }); - expect(onRequestUpdate).toBeCalledTimes(1); - - await afterTime(40); - await expect(serverApi.requestUpdateChart(chart2.id)).resolves.toMatchObject({ data: 1 }); - - await expect( - serverApi.getChartInfo(chart2.id).then((info) => info!.cacheList.map((item) => item.data)), - "通过 requestUpdateChart 获取的数据应该被推送到缓存", - ).resolves.toEqual([0, 1]); -}); - -describe.todo("updateSub", function () { - test("updateLine", function ({ vio }) { - const chart = vio.chart.create(2); - const data = [ - [0, 1, 3], - [2, 2, 3], - [3, 2, 3], - ]; - chart.updateData(data); - - // chart.updateSubData([7, 3, 9], 1); // 横向更新线 - - expect(chart.data).toEqual([ - [0, 1, 3], - [7, 3, 9], - [3, 2, 3], - ]); - - // chart.updateSubData([4, 5, 6], [undefined, 2]); // 纵向更新线 - // expect(chart.data).toEqual([ - // [0, 1, 4], - // [7, 3, 5], - // [3, 2, 6], - // ]); - - // chart.updateSubData(99, [1, 1]); // 更新点 - - // expect(chart.data).toEqual([ - // [0, 1, 4], - // [7, 99, 5], - // [3, 2, 6], - // ]); - }); - test("updateLine", function ({ vio }) { - const chart = vio.chart.create(1); - const data = [0, 1, 3]; - chart.updateData(data); - - // const [axis0, axis1] = chart.dimensionIndexNames; - }); -}); - -export function connectRpc(host: string) { - return connectWebsocket(`ws://${host}/api/rpc`).then((ws) => createWebSocketCpc(ws)); -} diff --git a/vio/test/tty/tty_api.test.ts b/vio/test/tty/tty_api.test.ts index 3881379..b97f08b 100644 --- a/vio/test/tty/tty_api.test.ts +++ b/vio/test/tty/tty_api.test.ts @@ -8,7 +8,7 @@ import { vioServerTest as test, VioServerTestContext as TestContext } from "../_ describe("tty-read", function () { beforeEach(async ({ connector }) => { const { clientApi, serverApi } = connector; - clientApi.sendTtyReadRequest.mockImplementation((index, reqId, req) => { + clientApi.tty.sendTtyReadRequest.mockImplementation((index, reqId, req) => { let res: any; switch (req.type) { case "text": @@ -26,11 +26,11 @@ describe("tty-read", function () { default: break; } - serverApi.resolveTtyReadRequest(index, reqId, res).then((res) => { + serverApi.tty.resolveTtyReadRequest(index, reqId, res).then((res) => { res; }); }); - const result = await serverApi.setTtyReadEnable(0, true); + const result = await serverApi.tty.setTtyReadEnable(0, true); }, 500); test("input", async function ({ task, vio }) { task.suite.tasks[0].result; @@ -47,10 +47,10 @@ describe("tty-read", function () { }); test("select", async function ({ vio, connector }) { const { clientApi, serverApi } = connector; - clientApi.sendTtyReadRequest.mockImplementationOnce((index, reqId, req) => { + clientApi.tty.sendTtyReadRequest.mockImplementationOnce((index, reqId, req) => { if (req.type === "select") { const res = (req as TtyInputReq.Select).options.map((item) => item.value); - serverApi.resolveTtyReadRequest(index, reqId, res); + serverApi.tty.resolveTtyReadRequest(index, reqId, res); } }); const res = await vio.select("title", [ @@ -65,7 +65,7 @@ describe("tty-read", function () { tty.writeText("xxxx"); await afterTime(50); - const calls = clientApi.writeTty.mock.calls; + const calls = clientApi.tty.writeTty.mock.calls; expect(calls[0][0], "id").toBe(8); expect(calls[0][1]).toEqual({ type: "text", title: "xxxx" } satisfies TtyOutputData.Text); }); @@ -75,18 +75,18 @@ test("重新获取输入权", async function ({ vio, connectVioSever }) { const c1 = await connectVioSever(); // 客户端连接 - await expect(c1.serverApi.setTtyReadEnable(0, true), "tty0 成功获取输入权").resolves.toBe(true); + await expect(c1.serverApi.tty.setTtyReadEnable(0, true), "tty0 成功获取输入权").resolves.toBe(true); - expect(c1.clientApi.sendTtyReadRequest, "应收到一次请求").toBeCalledTimes(1); + expect(c1.clientApi.tty.sendTtyReadRequest, "应收到一次请求").toBeCalledTimes(1); c1.cpc.onClose.catch(() => {}); c1.cpc.dispose(); // 模拟断开连接 const c2 = await connectVioSever(); //模拟重新连接 - await expect(c2.serverApi.setTtyReadEnable(0, true), "获取到输入权").resolves.toBe(true); - expect(c2.clientApi.sendTtyReadRequest, "应收到一次请求").toBeCalledTimes(1); - const reqId = c2.clientApi.sendTtyReadRequest.mock.calls[0][1]; - await expect(c2.serverApi.resolveTtyReadRequest(0, reqId, true), "c2 成功解决请求").resolves.toBe(true); + await expect(c2.serverApi.tty.setTtyReadEnable(0, true), "获取到输入权").resolves.toBe(true); + expect(c2.clientApi.tty.sendTtyReadRequest, "应收到一次请求").toBeCalledTimes(1); + const reqId = c2.clientApi.tty.sendTtyReadRequest.mock.calls[0][1]; + await expect(c2.serverApi.tty.resolveTtyReadRequest(0, reqId, true), "c2 成功解决请求").resolves.toBe(true); await expect(p1).resolves.toBe(true); }); @@ -94,19 +94,19 @@ test("切换输入权", async function ({ vio, connectVioSever }) { const c1 = await connectVioSever(); // 客户端连接 const p1 = vio.readText("h1"); //请求确认 - await expect(c1.serverApi.setTtyReadEnable(0, true), "tty0 成功获取输入权").resolves.toBe(true); - expect(c1.clientApi.sendTtyReadRequest, "应收到一次请求").toBeCalledTimes(1); + await expect(c1.serverApi.tty.setTtyReadEnable(0, true), "tty0 成功获取输入权").resolves.toBe(true); + expect(c1.clientApi.tty.sendTtyReadRequest, "应收到一次请求").toBeCalledTimes(1); const c2 = await connectVioSever(); //模拟另一个连接 - await expect(c2.serverApi.setTtyReadEnable(0, true), "成功夺取 tty0 的输入权").resolves.toBe(true); + await expect(c2.serverApi.tty.setTtyReadEnable(0, true), "成功夺取 tty0 的输入权").resolves.toBe(true); await afterTime(); - expect(c1.clientApi.ttyReadEnableChange, "通知c1 输入权被关闭").toBeCalledWith(0, false); - const reqId1 = c1.clientApi.sendTtyReadRequest.mock.calls[0][1]; - await expect(c1.serverApi.resolveTtyReadRequest(0, reqId1, "111"), "输入权被夺走,解决失败").resolves.toBe(false); + expect(c1.clientApi.tty.ttyReadEnableChange, "通知c1 输入权被关闭").toBeCalledWith(0, false); + const reqId1 = c1.clientApi.tty.sendTtyReadRequest.mock.calls[0][1]; + await expect(c1.serverApi.tty.resolveTtyReadRequest(0, reqId1, "111"), "输入权被夺走,解决失败").resolves.toBe(false); - const reqId2 = c2.clientApi.sendTtyReadRequest.mock.calls[0][1]; - await expect(c2.serverApi.resolveTtyReadRequest(0, reqId2, "222")).resolves.toBe(true); + const reqId2 = c2.clientApi.tty.sendTtyReadRequest.mock.calls[0][1]; + await expect(c2.serverApi.tty.resolveTtyReadRequest(0, reqId2, "222")).resolves.toBe(true); await expect(p1).resolves.toBe("222"); }); @@ -121,13 +121,13 @@ test("cache", async function ({ vio, connector }) { tty.writeText(i.toString()); cachedData.push(data); } - await expect(caller.getTtyCache(3)).resolves.toEqual(cachedData); + await expect(caller.tty.getTtyCache(3)).resolves.toEqual(cachedData); tty.writeText("hh"); - await expect(caller.getTtyCache(3)).resolves.toEqual([...cachedData.slice(1), { title: "hh", type: "text" }]); + await expect(caller.tty.getTtyCache(3)).resolves.toEqual([...cachedData.slice(1), { title: "hh", type: "text" }]); }); test("dispose", async function ({ vio, connector }) { const { clientApi, serverApi, cpc } = connector; - clientApi.sendTtyReadRequest.mockImplementation(() => new Promise(() => {})); + clientApi.tty.sendTtyReadRequest.mockImplementation(() => new Promise(() => {})); const tty = vio.tty.get(1); //创建 vio.tty.delete(tty); await expect(() => tty.readText()).rejects.toThrowError(); diff --git a/vio/test/vio.test.ts b/vio/test/vio.test.ts index c95c902..7ff6e2e 100644 --- a/vio/test/vio.test.ts +++ b/vio/test/vio.test.ts @@ -47,7 +47,7 @@ async function connectWs(url: string) { const ws = new WebSocket(url); ws.onopen = () => { resolve(ws); - ws.onerror = undefined; + ws.onerror = null; }; ws.onerror = (e: any) => { reject(e); diff --git a/vio/test/vio_objects/chart.test.ts b/vio/test/vio_objects/chart.test.ts new file mode 100644 index 0000000..dd84397 --- /dev/null +++ b/vio/test/vio_objects/chart.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, vi } from "vitest"; +import { connectWebsocket } from "../../src/lib/websocket.ts"; +import { ChartCreateOption, ChartInfo, DimensionInfo } from "@asla/vio"; +import { createWebSocketCpc } from "cpcall"; +import { afterTime } from "evlib"; +import { vioServerTest as test } from "../_env/test_port.ts"; +import { VioObjectCreateDto, VioObjectDto } from "../../src/vio/api_type.ts"; + +test("createChart-getObjects", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + function getList() { + return serverApi.object.getObjects().then((res) => res.list); + } + + await expect(getList()).resolves.toEqual([]); + + const config: ChartCreateOption = { name: "内存图", meta: { chartType: "progress" }, maxCacheSize: 20 }; + const chart = vio.object.createChart(1, config); //创建 + expect(chart).toMatchObject(config); + + const createdDto: VioObjectCreateDto = { id: chart.id, name: "内存图", type: "chart" }; + await expect(getList()).resolves.toEqual([createdDto] satisfies VioObjectDto[]); + + expect(clientApi.object.createObject).toBeCalledWith(createdDto); +}); + +test("dimensionsInfo", async function ({ vio }) { + const chart = vio.object.createChart(2, { dimensions: { 1: { name: "Y" } } }); //创建 + expect(chart.dimensions.length).toBe(2); + expect(chart.dimensions[1]).toMatchObject({ name: "Y" } satisfies DimensionInfo); +}); +test("dispose chart", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + const chart0 = vio.object.createChart(0); + const chart1 = vio.object.createChart(2); + await expect(serverApi.object.getObjects().then((res) => res.list)).resolves.toHaveLength(2); + let chart3 = vio.object.createChart(3); + await expect(serverApi.object.getObjects().then((res) => res.list)).resolves.toHaveLength(3); + + vio.object.disposeObject(chart1); + + await expect(serverApi.object.getObjects().then((res) => res.list.map((info) => info.id))).resolves.toEqual([0, 2]); + + expect(clientApi.object.deleteObject).toBeCalledWith(chart1.id); +}); +test("update", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + + const config: ChartCreateOption = { maxCacheSize: 20 }; + const chart = vio.object.createChart(1, config); //创建 + + await expect(serverApi.object.getChartInfo(chart.id)).resolves.toMatchObject({ + cacheList: [], + } satisfies Partial); + chart.updateData(1); //更新 + chart.updateData(2); //更新 + chart.updateData(3); //更新 + + expect(Array.from(chart.getCacheData())).toEqual([1, 2, 3]); + + await expect( + serverApi.object.getChartInfo(chart.id).then((res) => res?.cacheList.map((item) => item.data)), + ).resolves.toEqual([1, 2, 3]); + + expect(clientApi.object.writeChart).toBeCalledTimes(3); + + expect(clientApi.object.writeChart.mock.calls.map((item) => item[1])).toMatchObject([ + { data: 1 }, + { data: 2 }, + { data: 3 }, + ]); + + vio.object.disposeObject(chart); +}); + +test("cache", async function ({ vio }) { + const config: ChartCreateOption = { maxCacheSize: 4 }; + const chart = vio.object.createChart(1, config); //创建 + + for (let i = 0; i < chart.maxCacheSize; i++) { + chart.updateData(i); + } + expect(Array.from(chart.getCacheData())).toEqual([0, 1, 2, 3]); + chart.updateData(4); + expect(Array.from(chart.getCacheData())).toEqual([1, 2, 3, 4]); +}); + +test("Proactive update", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + const chart1 = vio.object.createChart(1); + + let i = 0; + const onRequestUpdate = vi.fn(() => i++); + const chart2 = vio.object.createChart(2, { onRequestUpdate, updateThrottle: 20 }); + + await expect( + serverApi.object.requestUpdateChart(chart1.id), + "Chart1 没有设置更新函数,应抛出异常", + ).rejects.toThrowError(); + await expect(serverApi.object.requestUpdateChart(chart2.id)).resolves.toMatchObject({ data: 0 }); + await expect( + serverApi.object.requestUpdateChart(chart2.id), + "请求频率超过设定的节流时间,返回的值还是原来的", + ).resolves.toMatchObject({ data: 0 }); + expect(onRequestUpdate).toBeCalledTimes(1); + + await afterTime(40); + await expect(serverApi.object.requestUpdateChart(chart2.id)).resolves.toMatchObject({ data: 1 }); + + await expect( + serverApi.object.getChartInfo(chart2.id).then((info) => info!.cacheList.map((item) => item.data)), + "通过 requestUpdateChart 获取的数据应该被推送到缓存", + ).resolves.toEqual([0, 1]); +}); + +describe.todo("updateSub", function () { + test("updateLine", function ({ vio }) { + const chart = vio.object.createChart(2); + const data = [ + [0, 1, 3], + [2, 2, 3], + [3, 2, 3], + ]; + chart.updateData(data); + + // chart.updateSubData([7, 3, 9], 1); // 横向更新线 + + expect(chart.data).toEqual([ + [0, 1, 3], + [7, 3, 9], + [3, 2, 3], + ]); + + // chart.updateSubData([4, 5, 6], [undefined, 2]); // 纵向更新线 + // expect(chart.data).toEqual([ + // [0, 1, 4], + // [7, 3, 5], + // [3, 2, 6], + // ]); + + // chart.updateSubData(99, [1, 1]); // 更新点 + + // expect(chart.data).toEqual([ + // [0, 1, 4], + // [7, 99, 5], + // [3, 2, 6], + // ]); + }); + test("updateLine", function ({ vio }) { + const chart = vio.object.createChart(1); + const data = [0, 1, 3]; + chart.updateData(data); + + // const [axis0, axis1] = chart.dimensionIndexNames; + }); +}); + +export function connectRpc(host: string) { + return connectWebsocket(`ws://${host}/api/rpc`).then((ws) => createWebSocketCpc(ws)); +} diff --git a/vio/test/vio_objects/table.test.ts b/vio/test/vio_objects/table.test.ts new file mode 100644 index 0000000..3d950e1 --- /dev/null +++ b/vio/test/vio_objects/table.test.ts @@ -0,0 +1,178 @@ +import { expect, vi, describe, beforeEach } from "vitest"; +import { vioServerTest as test } from "../_env/test_port.ts"; +import { Column, TableRenderFn, TableRow, Vio, VioTable } from "@asla/vio"; +import { TableChanges, TableDataDto, VioObjectCreateDto, VioTableDto } from "../../src/client.ts"; +import { UiButton } from "src/vio/vio_object/_ui/mod.ts"; +import { afterTime } from "evlib"; + +test("createTable", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + function getList() { + return serverApi.object.getObjects().then((res) => res.list); + } + const table = vio.object.createTable(columns, { + name: "进程", + keyField: "id", + }); + + const createDto: VioObjectCreateDto = { type: "table", name: "进程", id: table.id }; + await expect(getList()).resolves.toEqual([createDto] satisfies VioObjectCreateDto[]); + + expect(clientApi.object.createObject).toBeCalledWith(createDto); +}); + +test("getTable", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + + const table = vio.object.createTable(columns, { + name: "进程", + addAction: { text: "创建进程" }, + updateAction: true, + operations: commonOperation, + keyField: "id", + }); + + await expect(serverApi.object.getTable(table.id)).resolves.toEqual({ + columns, + operations: commonOperation, + addAction: { text: "创建进程" }, + name: "进程", + id: table.id, + keyField: "id", + updateAction: true, + } satisfies VioTableDto); +}); +describe("action", async function () { + type AddParam = Pick; + + function createTable(vio: Vio): VioTable { + return vio.object.createTable(columns, { + keyField: "id", + name: "进程", + addAction: { text: "创建进程" }, + // updateAction: true, // 默认为 true + operations: commonOperation, + }); + } + + test("getRows", async function ({ vio, connector }) { + const { clientApi, serverApi } = connector; + const table = createTable(vio); + table.updateTable([createRow(1), createRow(2), createRow(3), createRow(4)]); + const tablePage = await serverApi.object.getTableData(table.id, { skip: 1, number: 2 }); + const expectData: TableDataDto = { rows: [createRow(2), createRow(3)], index: [1, 2], total: 4 }; + expect(tablePage).toEqual(expectData); + }); + + test("addRow", async function ({ vio }) { + const table = createTable(vio); + table.addRow(createRow(0)); //0 + expect(() => table.addRow(createRow(1), 2)).toThrowError(RangeError); + expect(() => table.addRow(createRow(2), -1)).toThrowError(RangeError); + + table.addRow(createRow(1)); //1 + table.addRow(createRow(2), 0); //2 + table.addRow(createRow(3)); //3 + table.addRow(createRow(4), 3); //4 + + expect(table.getRows().rows.map((item) => item.id)).toEqual([2, 0, 1, 4, 3]); + }); + test("tableChange", async function ({ vio, connector }) { + const table = createTable(vio); + const { clientApi, serverApi } = connector; + table.updateTable([createRow(0), createRow(1)]); // 0,1 + + table.addRow(createRow(2)); + table.addRow(createRow(3)); + table.addRow(createRow(4)); // 0,1,2,3,4 add 2,3,4 + + table.updateRow(createRow(8), 1); // 0,8,2,3,4 update 1 + table.updateRow(createRow(9), 4); // 0,8,2,3,9 update 1 add 2,3,9 + table.deleteRow(1, 3); // 0,9 delete 1 add 9 + + const tableIdList = table.getRows().rows.map((item) => item.id); + expect(tableIdList).toEqual([0, 9]); + + await afterTime(200); + const calls = clientApi.object.tableChange.mock.calls[0]; + expect(calls).toEqual([ + table.id, + { + delete: [1], + // update: [], + add: [createRow(9)], + } satisfies TableChanges, + ]); + }); + + const onAction = vi.fn(); + beforeEach(() => { + onAction.mockRestore(); + }); + test("onAdd", async function ({ vio, connector }) { + const table = createTable(vio); + const { serverApi } = connector; + + table.onRowAdd = onAction; + const row: AddParam = { + args: ["-a"], + swapFile: "xxx", + }; + await serverApi.object.onTableRowAdd(table.id, row); + expect(onAction).toBeCalledWith(row); + }); + test("onRowAction", async function ({ vio, connector }) { + const table = createTable(vio); + const { serverApi } = connector; + table.onRowAction = onAction; + await serverApi.object.onTableRowAction(table.id, "k", "rowKey"); + expect(onAction).toBeCalledWith("k", "rowKey"); + }); + test("onTableAction", async function ({ vio, connector }) { + const table = createTable(vio); + const { serverApi } = connector; + table.onTableAction = onAction; + await serverApi.object.onTableAction(table.id, "k", [1, 2]); + expect(onAction).toBeCalledWith("k", [1, 2]); + }); + + let time = Date.now(); + function createRow(id: number): Process { + return { + args: ["-a"], + swapFile: "xxx", + createTime: time, + id, + status: 1, + }; + } +}); + +type Process = { + swapFile: string; + args: string[]; + createTime: number; + status: 0 | 1; + id: number; +}; + +const commonOperation: UiButton[] = [new UiButton("run", { text: "运行" }), new UiButton("stop", { text: "停止" })]; + +const actionRender: TableRenderFn = (args): UiButton[] => { + const { record } = args; + return [ + { key: "stop", ui: "button", props: { disable: record.status === 0, text: "停止", type: "text" } }, + { key: "run", ui: "button", props: { disable: record.status === 1, text: "启动", type: "text" } }, + ]; +}; + +const columns: Column[] = [ + { dataIndex: "swapFile" }, + { dataIndex: "args" }, + { dataIndex: "createTime", title: "创建时间" }, + { dataIndex: "status" }, + { + title: "操作", + render: actionRender.toString(), + }, +]; diff --git a/vio/tsconfig.build.json b/vio/tsconfig.build.json new file mode 100644 index 0000000..8da797b --- /dev/null +++ b/vio/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "temp", "/dist"] +}