Skip to content

Commit 3400733

Browse files
committed
feat: support streamlit custom component
1 parent d59d2e9 commit 3400733

12 files changed

+387
-286
lines changed

app/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"tailwind-merge": "^1.14.0",
4747
"tailwindcss": "^3.2.4",
4848
"tailwindcss-animate": "^1.0.7",
49-
"uuid": "^8.3.2"
49+
"uuid": "^8.3.2",
50+
"streamlit-component-lib": "^2.0.0"
5051
},
5152
"devDependencies": {
5253
"@rollup/plugin-commonjs": "^24.0.x",

app/src/index.tsx

+106-28
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import React, { useContext, useEffect, useState } from 'react';
22
import ReactDOM from 'react-dom';
33
import { observer } from "mobx-react-lite";
4-
import { autorun } from "mobx"
4+
import { reaction } from "mobx"
55
import { GraphicWalker, PureRenderer, GraphicRenderer, TableWalker } from '@kanaries/graphic-walker'
66
import type { VizSpecStore } from '@kanaries/graphic-walker/store/visualSpecStore'
77
import type { IGWHandler, IViewField, ISegmentKey, IDarkMode, IChatMessage, IRow } from '@kanaries/graphic-walker/interfaces';
88
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
9+
import { Streamlit, withStreamlitConnection } from "streamlit-component-lib"
910

1011
import Options from './components/options';
1112
import { IAppProps } from './interfaces';
1213

1314
import { loadDataSource, postDataService, finishDataService, getDatasFromKernelBySql, getDatasFromKernelByPayload } from './dataSource';
1415

1516
import commonStore from "./store/common";
16-
import { initJupyterCommunication, initHttpCommunication } from "./utils/communication";
17+
import { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback } from "./utils/communication";
1718
import communicationStore from "./store/communication"
1819
import { setConfig } from './utils/userConfig';
1920
import CodeExportModal from './components/codeExportModal';
@@ -41,7 +42,7 @@ import {
4142
ToggleGroup,
4243
ToggleGroupItem,
4344
} from "@/components/ui/toggle-group"
44-
import { SunIcon, MoonIcon, DesktopIcon } from "@radix-ui/react-icons"
45+
import { SunIcon, MoonIcon, DesktopIcon, ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
4546

4647
// @ts-ignore
4748
import style from './index.css?inline'
@@ -151,9 +152,17 @@ const ExploreApp: React.FC<IAppProps & {initChartFlag: boolean}> = (props) => {
151152
if (prop === "current") {
152153
if (value) {
153154
disposerRef.current?.();
154-
disposerRef.current = autorun(() => {
155-
setIsChanged((value as VizSpecStore).canUndo);
156-
});
155+
const store = value as VizSpecStore;
156+
disposerRef.current = reaction(
157+
() => store.currentVis,
158+
() => {
159+
setIsChanged((value as VizSpecStore).canUndo);
160+
streamlitComponentCallback({
161+
event: "spec_change",
162+
data: store.exportCode()
163+
});
164+
},
165+
);
157166
}
158167
}
159168
return Reflect.set(target, prop, value);
@@ -279,18 +288,45 @@ const ExploreApp: React.FC<IAppProps & {initChartFlag: boolean}> = (props) => {
279288
const PureRednererApp: React.FC<IAppProps> = observer((props) => {
280289
const computationCallback = getComputationCallback(props);
281290
const spec = props.visSpec[0];
291+
const [expand, setExpand] = useState(false);
282292

283293
return (
284294
<React.StrictMode>
285-
<PureRenderer
286-
{...props.extraConfig}
287-
name={spec.name}
288-
visualConfig={spec.config}
289-
visualLayout={spec.layout}
290-
visualState={spec.encodings}
291-
type='remote'
292-
computation={computationCallback!}
293-
/>
295+
<div className='flex'>
296+
{
297+
!expand && (<PureRenderer
298+
{...props.extraConfig}
299+
appearance={useContext(darkModeContext)}
300+
vizThemeConfig={props.themeKey}
301+
name={spec.name}
302+
visualConfig={spec.config}
303+
visualLayout={spec.layout}
304+
visualState={spec.encodings}
305+
type='remote'
306+
computation={computationCallback!}
307+
/>)
308+
}
309+
{
310+
expand && commonStore.isStreamlitComponent && (
311+
<div style={{minWidth: "96%"}}>
312+
<GraphicWalker
313+
{...props.extraConfig}
314+
appearance={useContext(darkModeContext)}
315+
vizThemeConfig={props.themeKey}
316+
fieldkeyGuard={props.fieldkeyGuard}
317+
fields={props.rawFields}
318+
data={props.useKernelCalc ? undefined : props.dataSource}
319+
computation={computationCallback}
320+
chart={props.visSpec}
321+
experimentalFeatures={{ computedField: props.useKernelCalc }}
322+
defaultConfig={{ config: { timezoneDisplayOffset: 0 } }}
323+
/>
324+
</div>
325+
)
326+
}
327+
{ commonStore.isStreamlitComponent && expand && ( <ChevronLeftIcon className='h-6 w-6 cursor-pointer border border-black-600 rounded-full'onClick={() => setExpand(false)}></ChevronLeftIcon> )}
328+
{ commonStore.isStreamlitComponent && !expand && ( <ChevronRightIcon className='h-6 w-6 cursor-pointer border border-black-600 rounded-full'onClick={() => setExpand(true)}></ChevronRightIcon> )}
329+
</div>
294330
</React.StrictMode>
295331
)
296332
});
@@ -338,18 +374,14 @@ function GWalkerComponent(props: IAppProps) {
338374
}
339375
}, []);
340376

341-
switch(props.gwMode) {
342-
case "explore":
343-
return <ExploreApp {...props} dataSource={dataSource} initChartFlag={initChartFlag} />;
344-
case "renderer":
345-
return <PureRednererApp {...props} dataSource={dataSource} />;
346-
case "filter_renderer":
347-
return <GraphicRendererApp {...props} dataSource={dataSource} />;
348-
case "table":
349-
return <TableWalkerApp {...props} dataSource={dataSource} />;
350-
default:
351-
return<ExploreApp {...props} dataSource={dataSource} initChartFlag={initChartFlag} />
352-
}
377+
return (
378+
<React.StrictMode>
379+
{ props.gwMode === "explore" && <ExploreApp {...props} dataSource={dataSource} initChartFlag={initChartFlag} /> }
380+
{ props.gwMode === "renderer" && <PureRednererApp {...props} dataSource={dataSource} /> }
381+
{ props.gwMode === "filter_renderer" && <GraphicRendererApp {...props} dataSource={dataSource} /> }
382+
{ props.gwMode === "table" && <TableWalkerApp {...props} dataSource={dataSource} /> }
383+
</React.StrictMode>
384+
)
353385
}
354386

355387
function GWalker(props: IAppProps, id: string) {
@@ -478,4 +510,50 @@ function TableWalkerApp(props: IAppProps) {
478510
)
479511
}
480512

481-
export default { GWalker, PreviewApp, ChartPreviewApp }
513+
514+
function SteamlitGWalkerApp(streamlitProps: any) {
515+
const props = streamlitProps.args as IAppProps;
516+
const [inited, setInited] = useState(false);
517+
const container = React.useRef(null);
518+
props.visSpec = FormatSpec(props.visSpec, props.rawFields);
519+
520+
useEffect(() => {
521+
commonStore.setIsStreamlitComponent(true);
522+
initOnHttpCommunication(props).then(() => {
523+
setInited(true);
524+
})
525+
}, []);
526+
527+
useEffect(() => {
528+
if (!container.current) return;
529+
const resizeObserver = new ResizeObserver(() => {
530+
Streamlit.setFrameHeight((container.current?.clientHeight ?? 0) + 20);
531+
})
532+
resizeObserver.observe(container.current);
533+
return () => resizeObserver.disconnect();
534+
}, [inited]);
535+
536+
return (
537+
<React.StrictMode>
538+
{inited && (
539+
<div ref={container}>
540+
<MainApp darkMode={props.dark}>
541+
<GWalkerComponent {...props} />
542+
</MainApp>
543+
</div>
544+
)}
545+
</React.StrictMode>
546+
);
547+
};
548+
549+
const StreamlitGWalker = () => {
550+
const StreamlitGWalker = withStreamlitConnection(SteamlitGWalkerApp);
551+
ReactDOM.render(
552+
<React.StrictMode>
553+
<StreamlitGWalker />
554+
</React.StrictMode>,
555+
document.getElementById("root")
556+
)
557+
}
558+
559+
export default { GWalker, PreviewApp, ChartPreviewApp, StreamlitGWalker }

app/src/store/common.ts

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class CommonStore {
2727
notification: INotification | null = null;
2828
uploadSpecModalOpen: boolean = false;
2929
uploadChartModalOpen: boolean = false;
30+
isStreamlitComponent: boolean = false;
3031

3132
setInitModalOpen(value: boolean) {
3233
this.initModalOpen = value;
@@ -60,6 +61,10 @@ class CommonStore {
6061
this.uploadChartModalOpen = value;
6162
}
6263

64+
setIsStreamlitComponent(value: boolean) {
65+
this.isStreamlitComponent = value;
66+
}
67+
6368
constructor() {
6469
makeObservable(this, {
6570
initModalOpen: observable,
@@ -69,13 +74,15 @@ class CommonStore {
6974
notification: observable,
7075
uploadSpecModalOpen: observable,
7176
uploadChartModalOpen: observable,
77+
isStreamlitComponent: observable,
7278
setInitModalOpen: action,
7379
setInitModalInfo: action,
7480
setShowCloudTool: action,
7581
setVersion: action,
7682
setNotification: action,
7783
setUploadSpecModalOpen: action,
7884
setUploadChartModalOpen: action,
85+
setIsStreamlitComponent: action
7986
});
8087
}
8188
}

app/src/utils/communication.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import commonStore from '../store/common';
3+
import { Streamlit } from "streamlit-component-lib"
34

45
interface IResponse {
56
data?: any;
@@ -208,6 +209,7 @@ const initHttpCommunication = async(gid: string, baseUrl: string) => {
208209
const basePath = window.parent.location.pathname.replace(/\/+$/, '').replace(/^\/+/, '');
209210
url = await getRealApiUrl(basePath, `${baseUrl}/${gid}`);
210211
}
212+
url = "/" + url.replace(new RegExp(`/*`), "");
211213

212214
const sendMsg = async(action: string, data: any, timeout: number = 30_000) => {
213215
const timer = setTimeout(() => {
@@ -247,5 +249,11 @@ const initHttpCommunication = async(gid: string, baseUrl: string) => {
247249
}
248250
}
249251

252+
const streamlitComponentCallback = (data: any) => {
253+
if (commonStore.isStreamlitComponent) {
254+
Streamlit.setComponentValue(data);
255+
}
256+
}
257+
250258
export type { ICommunication };
251-
export { initJupyterCommunication, initHttpCommunication };
259+
export { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback };

0 commit comments

Comments
 (0)