diff --git a/.github/workflows/auto-ci.yml b/.github/workflows/auto-ci.yml index 788fdef1..ae00b197 100644 --- a/.github/workflows/auto-ci.yml +++ b/.github/workflows/auto-ci.yml @@ -35,6 +35,11 @@ jobs: run: | chmod u+x ./scripts/compile.sh ./scripts/compile.sh + - name: Try Frontend Smoke Test + working-directory: ./app + run: | + yarn playwright install --with-deps chromium + yarn test:front_end - name: Uploading dist uses: actions/upload-artifact@v4 with: @@ -48,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os-version: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os-version }} @@ -64,6 +69,15 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'yarn' + cache-dependency-path: ./app/yarn.lock + - name: Ensure Yarn + run: | + npm install -g yarn - name: Download test data working-directory: ./ run: | @@ -75,32 +89,30 @@ jobs: PIP_ONLY_BINARY: "duckdb" run: | pip install ".[export]" + - name: Pin Pydantic v1 compatibility leg + if: ${{ matrix.os-version == 'ubuntu-latest' && matrix.python-version == '3.10' }} + run: | + pip install "pydantic==1.10.15" - name: Try Install Modin In Linux Os if: ${{ matrix.os-version == 'ubuntu-latest' && matrix.python-version == '3.11' }} run: | + pip install "numpy<2" "pandas<2.1" pip install aiohttp==3.8.6 pip install "modin==0.23.1" "modin[ray]==0.23.1" pip install pydantic==1.10.9 - name: Try Running working-directory: ./tests/ run: | - pip install ipykernel nbconvert pandas polars + pip install ipykernel nbmake pandas polars pytest python -m ipykernel install --name python --user jupyter kernelspec list - jupyter nbconvert --execute --ExecutePreprocessor.kernel_name=python --to html *.ipynb + python -c "from pathlib import Path; import subprocess, sys; notebooks = sorted(str(path) for path in Path('.').glob('*.ipynb')); subprocess.run([sys.executable, '-m', 'pytest', '--nbmake', '--nbmake-kernel=python', *notebooks], check=True)" - name: Try Pytest timeout-minutes: 20 working-directory: ./ run: | pip install duckdb_engine - pip install pytest - python ./scripts/ci_run_pytest.py - - name: Uploading notebooks - uses: actions/upload-artifact@v4 - if: ${{ matrix.python-version == '3.12' && matrix.os-version == 'ubuntu-latest' }} - with: - name: notebook - path: | - ./tests/main.html - ./tests/offline.html - ./tests/stress-test.html + pip install pytest ruff starlette polars tornado + ruff check pygwalker tests scripts bin pygwalker_tools + ruff format --check pygwalker tests scripts bin pygwalker_tools + python -X faulthandler -W error::DeprecationWarning:pygwalker -m pytest -o faulthandler_timeout=300 tests diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d5b8a304..eb215158 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,6 +56,15 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'yarn' + cache-dependency-path: ./app/yarn.lock + - name: Ensure Yarn + run: | + npm install -g yarn - name: Try building working-directory: ./ run: | diff --git a/.gitignore b/.gitignore index 682ec515..eff1ac28 100644 --- a/.gitignore +++ b/.gitignore @@ -130,7 +130,7 @@ dmypy.json __pycache__ dist -!/pygwalker/templates/dist/* +pygwalker/templates/dist/ .DS_Store venv .ipynb_checkpoints @@ -147,4 +147,4 @@ __pycache__/ *.db .web *.py[cod] -assets/external/ \ No newline at end of file +assets/external/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cb92b098 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing to PyGWalker + +The contributor workflow lives in [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md). + +Start there for the supported local setup, including: + +- Python editable installs and test commands. +- Frontend dependency installation, builds, type checks, and Playwright smoke tests. +- Optional local Graphic Walker source builds through `app`'s `dev:preinstall` script. +- Vite dev-server setup for JupyterLab hot reload. +- CI and package-build expectations. + +Keeping the detailed guide under `docs/` lets the development notes and +troubleshooting page link to one maintained source of truth. diff --git a/README.md b/README.md index 3ab3abc2..43afd73f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ [English](README.md) | [Español](./docs/README.es.md) | [Français](./docs/README.fr.md) | [Deutsch](./docs/README.de.md) | [中文](./docs/README.zh.md) | [Türkçe](./docs/README.tr.md) | [日本語](./docs/README.ja.md) | [한국어](./docs/README.ko.md) | [Русский](./docs/README.ru.md) +> [!NOTE] +> The English README is the source of truth for the API reference, installation, and development instructions. Translated READMEs are community-maintained and may lag behind this file. +

@@ -57,7 +60,7 @@ Visit [Google Colab](https://colab.research.google.com/drive/171QUQeq-uTLgSj1u-P # Features -PyGWalker is a Python library that simplifies data analysis and visualization workflows by turning pandas DataFrames into interactive visual interfaces. +PyGWalker is a Python library that simplifies data analysis and visualization workflows by turning pandas, polars, and pyarrow table data into interactive visual interfaces. It offers a variety of features that make it a powerful tool for data exploration: - ##### Interactive Data Exploration: - Drag-and-drop interface for easy visualization creation.   @@ -151,21 +154,45 @@ Cool things you can do with PyGwalker: ### Better Practices -There are some important parameters you should know when using pygwalker: +There are some important parameters you should know when using pygwalker: -+ `spec`: for save/load chart config (json string or file path) -+ `kernel_computation`: for using duckdb as computing engine which allows you to handle larger dataset faster in your local machine. -+ `use_kernel_calc`: Deprecated, use `kernel_computation` instead. ++ `spec_path`: local file path for saving/loading chart config. ++ `spec`: chart config object, JSON string, config ID, or remote URL. ++ `computation`: choose where data queries run. Use `"browser"` for frontend-only computation, `"kernel"` for local DuckDB-backed Python computation, `"cloud"` for Kanaries cloud computation, or omit it for the default automatic behavior. ++ `kernel_computation`: legacy boolean for using DuckDB as computing engine. Prefer `computation="kernel"`. ++ `use_kernel_calc`: Deprecated, use `computation="kernel"` or `kernel_computation` instead. ```python df = pd.read_csv('./bike_sharing_dc.csv') walker = pyg.walk( df, - spec="./chart_meta_0.json", # this json file will save your chart state, you need to click save button in ui mannual when you finish a chart, 'autosave' will be supported in the future. - kernel_computation=True, # set `kernel_computation=True`, pygwalker will use duckdb as computing engine, it support you explore bigger dataset(<=100GB). + spec_path="./chart_meta_0.json", # local file used to load and save chart state. + computation="kernel", # use DuckDB in the Python kernel for larger datasets. ) ``` +You can also create a reusable `Walker` object and choose where to render it: + +```python +walker = pyg.Walker(df, spec_path="./chart_meta_0.json", computation="browser") +walker.show() # auto-detects notebook or script mode +html = walker.to_html() +html = pyg.to_html(walker) +``` + +After exploring in the UI, export the current chart state as reproducible Python code: + +```python +code = walker.to_code(dataset_name="df") +print(code) +``` + +If you have an older saved spec, migrate it to the current schema before committing it: + +```python +migrated_spec = pyg.spec.migrate(open("./old_chart_meta.json").read()) +``` + ### Example in local notebook * Notebook Code: [Click Here](https://github.com/Kanaries/pygwalker-offline-example) @@ -181,7 +208,7 @@ walker = pyg.walk( After saving a chart from the UI, you can retrieve the image directly from Python. ```python -walker = pyg.walk(df, spec="./chart_meta_0.json") +walker = pyg.walk(df, spec_path="./chart_meta_0.json") # edit the chart in the UI and click the save button walker.save_chart_to_file("Chart 1", "chart1.svg", save_type="svg") png_bytes = walker.export_chart_png("Chart 1") @@ -216,7 +243,7 @@ st.title("Use Pygwalker In Streamlit") def get_pyg_renderer() -> "StreamlitRenderer": df = pd.read_csv("./bike_sharing_dc.csv") # If you want to use feature of saving chart config, set `spec_io_mode="rw"` - return StreamlitRenderer(df, spec="./gw_config.json", spec_io_mode="rw") + return StreamlitRenderer(df, spec_path="./gw_config.json", spec_io_mode="rw") renderer = get_pyg_renderer() @@ -224,24 +251,40 @@ renderer = get_pyg_renderer() renderer.explorer() ``` +If you already created a reusable `Walker`, Streamlit can render it directly: + +```python +import pygwalker as pyg +from pygwalker.api.streamlit import StreamlitRenderer + +walker = pyg.Walker(df, spec_path="./gw_config.json", computation="kernel") +renderer = StreamlitRenderer(walker) +renderer.explorer() +``` + ## [API Reference](https://pygwalker-docs.vercel.app/api-reference/jupyter) ### [pygwalker.walk](https://pygwalker-docs.vercel.app/api-reference/jupyter#walk) -| Parameter | Type | Default | Description | -|------------------------|-----------------------------------------------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| -| dataset | Union[DataFrame, Connector] | - | The dataframe or connector to be used. | -| gid | Union[int, str] | None | ID for the GraphicWalker container div, formatted as 'gwalker-{gid}'. | -| env | Literal['Jupyter', 'JupyterWidget'] | 'JupyterWidget' | Environment using pygwalker. | -| field_specs | Optional[Dict[str, FieldSpec]] | None | Specifications of fields. Will be automatically inferred from `dataset` if not specified. | -| hide_data_source_config | bool | True | If True, hides DataSource import and export button. | -| theme_key | Literal['vega', 'g2'] | 'g2' | Theme type for the GraphicWalker. | -| appearance | Literal['media', 'light', 'dark'] | 'media' | Theme setting. 'media' will auto-detect the OS theme. | -| spec | str | "" | Chart configuration data. Can be a configuration ID, JSON, or remote file URL. | -| use_preview | bool | True | If True, uses the preview function. | -| kernel_computation | bool | False | If True, uses kernel computation for data. | -| **kwargs | Any | - | Additional keyword arguments. | +| Parameter | Type | Default | Description | +|--------------------|-----------------------------------------------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------| +| dataset | Union[DataFrame, pyarrow.Table, Connector, str, Walker] | - | DataFrame, pyarrow table, database connector, SQL/data source string, or reusable Walker object to explore. | +| gid | Union[int, str] | None | ID for the GraphicWalker container div, formatted as `gwalker-{gid}`. | +| env | Literal['JupyterAnywidget', 'Jupyter', 'JupyterWidget'] | 'JupyterAnywidget' | Notebook rendering environment. Use `JupyterAnywidget` or omit `env`; `Jupyter` and `JupyterWidget` are deprecated legacy transports kept for compatibility. | +| field_specs | Optional[List[FieldSpec]] | None | Field specifications. They will be inferred from `dataset` if not specified. | +| theme_key | Literal['vega', 'g2', 'streamlit'] | 'g2' | Theme type for Graphic Walker. | +| appearance | Literal['media', 'light', 'dark'] | 'media' | Theme appearance. `media` follows the operating system preference. | +| spec | str | "" | Chart configuration data. Can be a configuration ID, JSON string, local file path, or remote file URL. | +| spec_path | Optional[str] | None | Local chart configuration file path. Prefer this over passing a local file path through `spec`. | +| computation | Optional[Literal['auto', 'browser', 'kernel', 'cloud']] | None | Computation backend. Omit it for automatic behavior; use `browser`, `kernel`, or `cloud` to choose explicitly. | +| use_kernel_calc | Optional[bool] | None | Deprecated. Use `computation="kernel"` or `kernel_computation` instead. | +| kernel_computation | Optional[bool] | None | Legacy boolean for local DuckDB-backed kernel computation. Prefer `computation="kernel"` or `computation="browser"`. | +| cloud_computation | bool | False | Legacy boolean for Kanaries cloud computation. Prefer `computation="cloud"`. | +| show_cloud_tool | bool | True | Whether to show the Kanaries cloud tool when available. | +| kanaries_api_key | str | "" | Kanaries API key used by cloud features. | +| default_tab | Literal['data', 'vis'] | 'vis' | Default tab to show when the UI opens. | +| **kwargs | Any | - | Additional keyword arguments. | ## Development @@ -257,7 +300,7 @@ Refer it: [local-development](https://docs.kanaries.net/pygwalker/installation#l - [x] Databricks Notebook (Since version `0.1.4a0`) - [x] Jupyter Extension for Visual Studio Code (Since version `0.1.4a0`) - [x] Most web applications compatiable with IPython kernels. (Since version `0.1.4a0`) -- [x] **Streamlit (Since version `0.1.4.9`)**, enabled with `pyg.walk(df, env='Streamlit')` +- [x] **Streamlit (Since version `0.1.4.9`)**, enabled with `pygwalker.api.streamlit.StreamlitRenderer` - [x] DataCamp Workspace (Since version `0.1.4a0`) - [x] Panel. See [panel-graphic-walker](https://github.com/panel-extensions/panel-graphic-walker). - [x] marimo (Since version `0.4.9.11`) @@ -276,7 +319,7 @@ usage: pygwalker config [-h] [--set [key=value ...]] [--reset [key ...]] [--rese Modify configuration file. (default: ~/Library/Application Support/pygwalker/config.json) Available configurations: -- privacy ['offline', 'update-only', 'events'] (default: events). +- privacy ['offline', 'update-only', 'events'] (default: update-only). "offline": fully offline, no data is send or api is requested "update-only": only check whether this is a new version of pygwalker to update "events": share which events about which feature is used in pygwalker, it only contains events data about which feature you arrive for product optimization. No DATA YOU ANALYSIS IS SEND. Events data will bind with a unique id, which is generated by pygwalker when it is installed based on timestamp. We will not collect any other information about you. diff --git a/app/.gitignore b/app/.gitignore index 0b0750c9..569eee4d 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,4 @@ node_modules/ -!lib \ No newline at end of file +test-results/ +playwright-report/ +!lib diff --git a/app/package.json b/app/package.json index e55b08e7..f1be59d1 100644 --- a/app/package.json +++ b/app/package.json @@ -5,13 +5,14 @@ "license": "Apache License 2.0", "private": true, "scripts": { - "build": "vite build && vite build --mode=dsl_to_workflow && vite build --mode=vega_to_dsl", + "build": "yarn typecheck && vite build && vite build --mode=dsl_to_workflow && vite build --mode=vega_to_dsl", "build:app": "vite build", + "typecheck": "tsc --noEmit --pretty false", "dev:preinstall": "(cd ../graphic-walker/packages/graphic-walker; yarn --frozen-lockfile && yarn build)", "dev:server": "vite --host", "dev": "vite", - "test:front_end": "vite --host", - "test": "npm run test:front_end", + "test:front_end": "playwright test", + "test": "yarn test:front_end", "serve": "vite preview" }, "dependencies": { @@ -29,7 +30,6 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", - "@segment/analytics-next": "^1.69.0", "autoprefixer": "^10.3.5", "buffer": "^6.0.3", "class-variance-authority": "^0.7.0", @@ -50,6 +50,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@playwright/test": "^1.61.0", "@rollup/plugin-commonjs": "^24.0.x", "@rollup/plugin-replace": "^5.0.x", "@rollup/plugin-terser": "^0.4.x", @@ -59,9 +60,9 @@ "@types/react-dom": "^19.x", "@types/react-syntax-highlighter": "^15.5.7", "@types/styled-components": "^5.1.26", - "@vitejs/plugin-react": "^3.1.x", + "@vitejs/plugin-react": "^4.7.0", "typescript": "^5.4.2", - "vite": "4.1.5", + "vite": "^6.4.3", "vite-plugin-wasm": "^3.2.2" }, "resolutions": { diff --git a/app/playwright.config.ts b/app/playwright.config.ts new file mode 100644 index 00000000..905988cc --- /dev/null +++ b/app/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + timeout: 30_000, + expect: { + timeout: 10_000, + }, + use: { + ...devices["Desktop Chrome"], + baseURL: "http://127.0.0.1:8769", + trace: "on-first-retry", + }, + webServer: { + command: "yarn dev --host 127.0.0.1", + url: "http://127.0.0.1:8769", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/app/src/components/uploadChartModal/index.tsx b/app/src/components/uploadChartModal/index.tsx index c8200a99..28b4298b 100644 --- a/app/src/components/uploadChartModal/index.tsx +++ b/app/src/components/uploadChartModal/index.tsx @@ -4,6 +4,10 @@ import type { IGWHandler } from "@kanaries/graphic-walker/interfaces"; import type { VizSpecStore } from '@kanaries/graphic-walker/store/visualSpecStore' import { chartToWorkflow } from "@kanaries/graphic-walker" import { tracker } from "@/utils/tracker"; +import type { + ICommUploadCloudChartRequest, + ICommUploadCloudDashboardRequest, +} from "@/interfaces"; import communicationStore from "../../store/communication"; import commonStore from "../../store/common"; @@ -92,32 +96,34 @@ const UploadChartModal: React.FC = observer((props) => { const visSpec = props.storeRef.current?.exportCode()!; try { if (instanceType === "dashboard") { + const request: ICommUploadCloudDashboardRequest = { + chartName, + datasetName, + isPublic, + isCreateDashboard, + visSpec, + workflowList: visSpec.map((spec) => chartToWorkflow(spec).workflow), + }; const resp = await communicationStore.comm?.sendMsg( "upload_to_cloud_dashboard", - { - chartName: chartName, - datasetName: datasetName, - isPublic: isPublic, - isCreateDashboard: isCreateDashboard, - visSpec: visSpec, - workflowList: visSpec.map(spec => chartToWorkflow(spec).workflow), - }, + request, 120_000 ); - uploadSuccess(instanceType, resp?.data.dashboardId, resp?.data.datasetId); + uploadSuccess(instanceType, resp?.data?.dashboardId ?? "", resp?.data?.datasetId ?? ""); } else { + const request: ICommUploadCloudChartRequest = { + chartName, + datasetName, + isPublic, + visSpec, + workflow: chartToWorkflow(visSpec[0]).workflow, + }; const resp = await communicationStore.comm?.sendMsg( "upload_to_cloud_charts", - { - chartName: chartName, - datasetName: datasetName, - isPublic: isPublic, - visSpec: visSpec, - workflow: chartToWorkflow(visSpec[0]).workflow, - }, + request, 120_000 ); - uploadSuccess(instanceType, resp?.data.chartId, resp?.data.datasetId); + uploadSuccess(instanceType, resp?.data?.chartId ?? "", resp?.data?.datasetId ?? ""); } commonStore.setUploadChartModalOpen(false); } finally { diff --git a/app/src/components/uploadSpecModal/index.tsx b/app/src/components/uploadSpecModal/index.tsx index 9bfa1079..660ae750 100644 --- a/app/src/components/uploadSpecModal/index.tsx +++ b/app/src/components/uploadSpecModal/index.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import type { VizSpecStore } from '@kanaries/graphic-walker/store/visualSpecStore' import { chartToWorkflow } from "@kanaries/graphic-walker/utils/workflow"; import { tracker } from "@/utils/tracker"; +import type { ICommUploadSpecToCloudRequest } from "@/interfaces"; import communicationStore from "../../store/communication"; import commonStore from "../../store/common"; @@ -49,13 +50,14 @@ const UploadSpecModal: React.FC = observer((props) => { try { + const request: ICommUploadSpecToCloudRequest = { fileName: specName, newToken: isSetToken ? token : "" }; const resp = await communicationStore.comm?.sendMsg( "upload_spec_to_cloud", - {"fileName": specName, "newToken": isSetToken ? token : ""}, + request, 30_000 ); commonStore.setUploadSpecModalOpen(false); - uploadSuccess(resp?.data["specFilePath"]); + uploadSuccess(resp?.data?.specFilePath ?? ""); props.setGwIsChanged(false); } finally { setUploading(false); diff --git a/app/src/dataSource/index.tsx b/app/src/dataSource/index.tsx index f7960197..760b41a2 100644 --- a/app/src/dataSource/index.tsx +++ b/app/src/dataSource/index.tsx @@ -3,6 +3,7 @@ import type { IRow, IDataQueryPayload } from "@kanaries/graphic-walker/interface import commonStore from "../store/common"; import communicationStore from "../store/communication" import { parser_dsl_with_meta } from "@kanaries/gw-dsl-parser"; +import type { ICommunication } from "../utils/communication"; interface MessagePayload extends IDataSourceProps { action: "requestData" | "postData" | "finishData"; @@ -78,24 +79,33 @@ export function finishDataService(msg: any) { ) } -interface IBatchGetDatasTask { - query: any; - resolve: (value: any) => void; +interface IBatchGetDatasTask { + query: TQuery; + resolve: (value: IRow[]) => void; reject: (reason?: any) => void; } -function initBatchGetDatas(action: string) { - const taskList = [] as IBatchGetDatasTask[]; +function initBatchGetDatas( + action: "batch_get_datas_by_sql" | "batch_get_datas_by_payload", + comm: ICommunication | null +) { + const taskList = [] as IBatchGetDatasTask[]; - const batchGetDatas = async(taskList: IBatchGetDatasTask[]) => { - const result = await communicationStore.comm?.sendMsg( - action, - {"queryList": taskList.map(task => task.query)}, - 60_000 - ); - if (result) { + const batchGetDatas = async(taskList: IBatchGetDatasTask[]) => { + const result = action === "batch_get_datas_by_sql" + ? await comm?.sendMsg( + action, + { queryList: taskList.map(task => task.query as string) }, + 60_000 + ) + : await comm?.sendMsg( + action, + { queryList: taskList.map(task => task.query as IDataQueryPayload) }, + 60_000 + ); + if (result?.data?.datas) { for (let i = 0; i < taskList.length; i++) { - taskList[i].resolve(result["data"]["datas"][i]); + taskList[i].resolve(result.data.datas[i]); } } else { for (let i = 0; i < taskList.length; i++) { @@ -104,8 +114,8 @@ function initBatchGetDatas(action: string) { } } - const getDatas = (query: any) => { - return new Promise((resolve, reject) => { + const getDatas = (query: TQuery) => { + return new Promise((resolve, reject) => { taskList.push({ query, resolve, reject }); if (taskList.length === 1) { setTimeout(() => { @@ -120,8 +130,6 @@ function initBatchGetDatas(action: string) { } } -const batchGetDatasBySql = initBatchGetDatas("batch_get_datas_by_sql"); -const batchGetDatasByPayload = initBatchGetDatas("batch_get_datas_by_payload"); const DEFAULT_LIMIT = 50_000; function notifyDataLimit() { @@ -144,7 +152,8 @@ function notifyDataLimit() { }, 60_000); } -export function getDatasFromKernelBySql(fieldMetas: any) { +export function getDatasFromKernelBySql(fieldMetas: any, comm: ICommunication | null) { + const batchGetDatasBySql = initBatchGetDatas("batch_get_datas_by_sql", comm); return async (payload: IDataQueryPayload) => { const sql = parser_dsl_with_meta( "pygwalker_mid_table", @@ -159,10 +168,13 @@ export function getDatasFromKernelBySql(fieldMetas: any) { } } -export async function getDatasFromKernelByPayload(payload: IDataQueryPayload) { - const result = await batchGetDatasByPayload.getDatas({...payload, limit: payload.limit ?? DEFAULT_LIMIT}) ?? []; - if (!payload.limit && result.length === DEFAULT_LIMIT) { - notifyDataLimit(); +export function getDatasFromKernelByPayload(comm: ICommunication | null) { + const batchGetDatasByPayload = initBatchGetDatas("batch_get_datas_by_payload", comm); + return async (payload: IDataQueryPayload) => { + const result = await batchGetDatasByPayload.getDatas({...payload, limit: payload.limit ?? DEFAULT_LIMIT}) ?? []; + if (!payload.limit && result.length === DEFAULT_LIMIT) { + notifyDataLimit(); + } + return result as IRow[]; } - return result as IRow[]; } diff --git a/app/src/index.tsx b/app/src/index.tsx index 84f6318e..320181dd 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { Suspense, useCallback, useContext, useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { observer } from "mobx-react-lite"; import { reaction } from "mobx" @@ -10,7 +10,12 @@ import { Streamlit, withStreamlitConnection } from "streamlit-component-lib" import { createRender, useModel } from "@anywidget/react"; import Options from './components/options'; -import { IAppProps } from './interfaces'; +import type { + IAppProps, + ICommAskSpecRequest, + ICommChatChartRequest, + ICommSaveChartRequest, +} from './interfaces'; import { loadDataSource, postDataService, finishDataService, getDatasFromKernelBySql, getDatasFromKernelByPayload } from './dataSource'; @@ -18,12 +23,8 @@ import commonStore from "./store/common"; import { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback, initAnywidgetCommunication } from "./utils/communication"; import communicationStore from "./store/communication" import { setConfig } from './utils/userConfig'; -import CodeExportModal from './components/codeExportModal'; import type { IPreviewProps, IChartPreviewProps } from './components/preview'; import { Preview, ChartPreview } from './components/preview'; -import UploadSpecModal from "./components/uploadSpecModal" -import UploadChartModal from './components/uploadChartModal'; -import InitModal from './components/initModal'; import { getSaveTool } from './tools/saveTool'; import { getExportTool } from './tools/exportTool'; import { getExportDataframeTool } from './tools/exportDataframe'; @@ -54,6 +55,48 @@ import { getOpenDesktopTool } from './tools/openDesktop'; import RuncellBanner from './components/runcellBanner'; +const InitModal = React.lazy(() => import("./components/initModal")); +const UploadSpecModal = React.lazy(() => import("./components/uploadSpecModal")); +const UploadChartModal = React.lazy(() => import("./components/uploadChartModal")); +const CodeExportModal = React.lazy(() => import("./components/codeExportModal")); + +const ExploreModals = observer((props: { + exportOpen: boolean; + setExportOpen: React.Dispatch>; + gwRef: React.MutableRefObject; + storeRef: React.MutableRefObject; + setGwIsChanged: React.Dispatch>; + sourceCode: string; +}) => { + const darkMode = useContext(darkModeContext); + + return ( + <> + {commonStore.uploadSpecModalOpen && ( + + + + )} + {commonStore.uploadChartModalOpen && ( + + + + )} + {props.exportOpen && ( + + + + )} + + ); +}); + + const initChart = async (gwRef: React.MutableRefObject, total: number, props: IAppProps) => { if (props.needInitChart && props.env === "jupyter_widgets" && total !== 0) { commonStore.setInitModalOpen(true); @@ -63,7 +106,8 @@ const initChart = async (gwRef: React.MutableRefObject, total total: total, }); for await (const chart of gwRef.current?.exportChartList("data-url")!) { - await communicationStore.comm?.sendMsg("save_chart", await formatExportedChartDatas(chart.data)); + const request = await formatExportedChartDatas(chart.data) as ICommSaveChartRequest; + await communicationStore.comm?.sendMsg("save_chart", request); commonStore.setInitModalInfo({ title: "Recover Charts", curIndex: chart.index + 1, @@ -75,11 +119,12 @@ const initChart = async (gwRef: React.MutableRefObject, total } const getComputationCallback = (props: IAppProps) => { + const comm = props.__comm ?? communicationStore.comm; if (props.useKernelCalc && props.parseDslType === "client") { - return getDatasFromKernelBySql(props.fieldMetas); + return getDatasFromKernelBySql(props.fieldMetas, comm); } if (props.useKernelCalc && props.parseDslType === "server") { - return getDatasFromKernelByPayload; + return getDatasFromKernelByPayload(comm); } } @@ -143,7 +188,11 @@ const MainApp = observer((props: {children: React.ReactNode, darkMode: "dark" | )} - + {commonStore.initModalOpen && ( + + + + )}
@@ -160,7 +209,7 @@ const ExploreApp: React.FC = (props) => { const [hideModeOption, _] = useState(true); const [isChanged, setIsChanged] = useState(false); const storeRef = React.useRef(null); - const disposerRef = React.useRef<() => void>(); + const disposerRef = React.useRef<(() => void) | undefined>(undefined); const storeRefProxied = React.useMemo( () => new Proxy(storeRef, { @@ -196,19 +245,23 @@ const ExploreApp: React.FC = (props) => { setConfig(userConfig); tracker.setOpen(userConfig.privacy === "events"); }; - }, []); + }, [props.showCloudTool, props.hashcode, userConfig]); + + useEffect(() => { + setVisSpec(props.visSpec); + }, [props.visSpec]); useEffect(() => { if (props.initChartFlag) { setTimeout(() => { initChart(gwRef, visSpec.length, props) }, 0); } - }, [props.initChartFlag]); + }, [props.initChartFlag, props.id, props.visSpec, visSpec.length]); useEffect(() => { setTimeout(() => { storeRef.current?.setSegmentKey(props.defaultTab as ISegmentKey); }, 0); - }, [mode]); + }, [mode, props.defaultTab]); const runcellTool = getRuncellTool(); const exportTool = getExportTool(setExportOpen); @@ -234,14 +287,16 @@ const ExploreApp: React.FC = (props) => { const features: Record = {}; if (props.enableAskViz) { features["askviz"] = async (metas: IViewField[], query: string) => { - const resp = await communicationStore.comm?.sendMsg("get_spec_by_text", { metas, query }); - return resp?.data.data; + const request: ICommAskSpecRequest = { metas, query }; + const resp = await communicationStore.comm?.sendMsg("get_spec_by_text", request); + return resp?.data?.data; }; } if (props.enableVlChat) { features["vlChat"] = async (metas: IViewField[], chats: IChatMessage[]) => { - const resp = await communicationStore.comm?.sendMsg("get_chart_by_chats", { metas, chats }); - return resp?.data.data; + const request: ICommChatChartRequest = { metas, chats }; + const resp = await communicationStore.comm?.sendMsg("get_chart_by_chats", request); + return resp?.data?.data; }; } if (Object.keys(features).length > 0) { @@ -251,7 +306,10 @@ const ExploreApp: React.FC = (props) => { return undefined; }, [props.showCloudTool, props.enableAskViz, props.enableVlChat]); - const computationCallback = React.useMemo(() => getComputationCallback(props), []); + const computationCallback = React.useMemo( + () => getComputationCallback(props), + [props.useKernelCalc, props.parseDslType, props.fieldMetas, props.__comm], + ); const modeChange = (value: string) => { if (mode === "walker") { @@ -263,9 +321,14 @@ const ExploreApp: React.FC = (props) => { return ( - - - + { !hideModeOption &&