Skip to content

Commit b60ab32

Browse files
committed
PerspectiveWorkspace React component
Signed-off-by: Davis Silverman <davis@thedav.is>
1 parent 22bda2f commit b60ab32

17 files changed

Lines changed: 857 additions & 175 deletions

File tree

packages/perspective-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"peerDependencies": {
2525
"@finos/perspective": "workspace:^",
2626
"@finos/perspective-viewer": "workspace:^",
27+
"@finos/perspective-workspace": "workspace:^",
2728
"@types/react": "^18",
2829
"react": "^18",
2930
"react-dom": "^18"

packages/perspective-react/src/index.tsx

Lines changed: 2 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -20,87 +20,5 @@
2020
* @module
2121
*/
2222

23-
import * as React from "react";
24-
import type * as psp from "@finos/perspective";
25-
import type * as pspViewer from "@finos/perspective-viewer";
26-
27-
function usePspListener<A>(
28-
viewer: HTMLElement | null,
29-
name: string,
30-
f?: (x: A) => void
31-
) {
32-
React.useEffect(() => {
33-
if (!f) return;
34-
const ctx = new AbortController();
35-
const callback = (e: Event) => f((e as CustomEvent).detail);
36-
viewer?.addEventListener(name, callback, { signal: ctx.signal });
37-
return () => ctx.abort();
38-
}, [viewer, f]);
39-
}
40-
41-
export interface PerspectiveViewerProps {
42-
table?: psp.Table | Promise<psp.Table>;
43-
config?: pspViewer.ViewerConfigUpdate;
44-
onConfigUpdate?: (config: pspViewer.ViewerConfigUpdate) => void;
45-
onClick?: (data: pspViewer.PerspectiveClickEventDetail) => void;
46-
onSelect?: (data: pspViewer.PerspectiveSelectEventDetail) => void;
47-
48-
// Applicable props from `React.HTMLAttributes`, which we cannot extend
49-
// directly because Perspective changes the signature of `onClick`.
50-
className?: string | undefined;
51-
hidden?: boolean | undefined;
52-
id?: string | undefined;
53-
slot?: string | undefined;
54-
style?: React.CSSProperties | undefined;
55-
tabIndex?: number | undefined;
56-
title?: string | undefined;
57-
}
58-
59-
function PerspectiveViewerImpl(props: PerspectiveViewerProps) {
60-
const [viewer, setViewer] =
61-
React.useState<pspViewer.HTMLPerspectiveViewerElement | null>(null);
62-
63-
React.useEffect(() => {
64-
return () => {
65-
viewer?.delete();
66-
};
67-
}, [viewer]);
68-
69-
React.useEffect(() => {
70-
if (props.table) {
71-
viewer?.load(props.table);
72-
} else {
73-
viewer?.eject();
74-
}
75-
}, [viewer, props.table]);
76-
77-
React.useEffect(() => {
78-
if (props.table && props.config) {
79-
viewer?.restore(props.config);
80-
}
81-
}, [viewer, props.table, JSON.stringify(props.config)]);
82-
83-
usePspListener(viewer, "perspective-click", props.onClick);
84-
usePspListener(viewer, "perspective-select", props.onSelect);
85-
usePspListener(viewer, "perspective-config-update", props.onConfigUpdate);
86-
87-
return (
88-
<perspective-viewer
89-
ref={setViewer}
90-
id={props.id}
91-
className={props.className}
92-
hidden={props.hidden}
93-
slot={props.slot}
94-
style={props.style}
95-
tabIndex={props.tabIndex}
96-
title={props.title}
97-
/>
98-
);
99-
}
100-
101-
/**
102-
* A React wrapper component for `<perspective-viewer>` Custom Element.
103-
*/
104-
export const PerspectiveViewer: React.FC<PerspectiveViewerProps> = React.memo(
105-
PerspectiveViewerImpl
106-
);
23+
export * from "./viewer";
24+
export * from "./workspace";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
import * as React from "react";
14+
15+
export function usePspListener<A>(
16+
el: HTMLElement | undefined,
17+
event: string,
18+
cb?: (x: A) => void
19+
) {
20+
React.useEffect(() => {
21+
if (!cb || !el) return;
22+
const ctx = new AbortController();
23+
const callback = (e: Event) => cb((e as CustomEvent).detail);
24+
el?.addEventListener(event, callback, { signal: ctx.signal });
25+
return () => ctx.abort();
26+
}, [el, cb]);
27+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
import type * as psp from "@finos/perspective";
14+
import type * as pspViewer from "@finos/perspective-viewer";
15+
16+
import * as utils from "./utils";
17+
18+
import * as React from "react";
19+
20+
export interface PerspectiveViewerProps {
21+
table?: psp.Table | Promise<psp.Table>;
22+
config?: pspViewer.ViewerConfigUpdate;
23+
onConfigUpdate?: (config: pspViewer.ViewerConfigUpdate) => void;
24+
onClick?: (data: pspViewer.PerspectiveClickEventDetail) => void;
25+
onSelect?: (data: pspViewer.PerspectiveSelectEventDetail) => void;
26+
27+
// Applicable props from `React.HTMLAttributes`, which we cannot extend
28+
// directly because Perspective changes the signature of `onClick`.
29+
className?: string | undefined;
30+
hidden?: boolean | undefined;
31+
id?: string | undefined;
32+
slot?: string | undefined;
33+
style?: React.CSSProperties | undefined;
34+
tabIndex?: number | undefined;
35+
title?: string | undefined;
36+
}
37+
38+
function PerspectiveViewerImpl(props: PerspectiveViewerProps) {
39+
const [viewer, setViewer] =
40+
React.useState<pspViewer.HTMLPerspectiveViewerElement>();
41+
42+
React.useEffect(() => {
43+
return () => {
44+
viewer?.delete();
45+
};
46+
}, [viewer]);
47+
48+
React.useEffect(() => {
49+
if (props.table) {
50+
viewer?.load(props.table);
51+
} else {
52+
viewer?.eject();
53+
}
54+
}, [viewer, props.table]);
55+
56+
React.useEffect(() => {
57+
if (viewer && props.table && props.config) {
58+
viewer.restore(props.config);
59+
}
60+
}, [viewer, props.table, JSON.stringify(props.config)]);
61+
62+
utils.usePspListener(viewer, "perspective-click", props.onClick);
63+
utils.usePspListener(viewer, "perspective-select", props.onSelect);
64+
utils.usePspListener(
65+
viewer,
66+
"perspective-config-update",
67+
props.onConfigUpdate
68+
);
69+
70+
return (
71+
<perspective-viewer
72+
ref={(r) => setViewer(r ?? undefined)}
73+
id={props.id}
74+
className={props.className}
75+
hidden={props.hidden}
76+
slot={props.slot}
77+
style={props.style}
78+
tabIndex={props.tabIndex}
79+
title={props.title}
80+
/>
81+
);
82+
}
83+
84+
/**
85+
* A React wrapper component for `<perspective-viewer>` Custom Element.
86+
*/
87+
export const PerspectiveViewer: React.FC<PerspectiveViewerProps> = React.memo(
88+
PerspectiveViewerImpl
89+
);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
import type * as psp from "@finos/perspective";
14+
import type * as pspWorkspace from "@finos/perspective-workspace";
15+
import {
16+
PerspectiveWorkspaceConfig,
17+
ViewerConfigUpdateExt,
18+
} from "@finos/perspective-workspace";
19+
20+
import * as utils from "./utils";
21+
22+
import * as React from "react";
23+
24+
interface PerspectiveWorkspaceProps {
25+
tables: Record<string, Promise<psp.Table>>;
26+
layout: PerspectiveWorkspaceConfig;
27+
onLayoutUpdate?: (detail: {
28+
layout: PerspectiveWorkspaceConfig;
29+
tables: Record<string, psp.Table | Promise<psp.Table>>;
30+
}) => void;
31+
onNewView?: (detail: {
32+
config: ViewerConfigUpdateExt;
33+
widget: pspWorkspace.PerspectiveViewerWidget;
34+
}) => void;
35+
onToggleGlobalFilter?: (detail: {
36+
widget: pspWorkspace.PerspectiveViewerWidget;
37+
isGlobalFilter: boolean;
38+
}) => void;
39+
id?: string;
40+
className?: string;
41+
style?: React.CSSProperties | undefined;
42+
}
43+
44+
const PerspectiveWorkspaceImpl = React.forwardRef<
45+
pspWorkspace.HTMLPerspectiveWorkspaceElement | undefined,
46+
PerspectiveWorkspaceProps
47+
>(
48+
(
49+
{
50+
tables,
51+
layout,
52+
onLayoutUpdate = () => {},
53+
onNewView = () => {},
54+
onToggleGlobalFilter = () => {},
55+
id,
56+
className,
57+
style,
58+
},
59+
ref
60+
) => {
61+
const [workspace, setWorkspace] =
62+
React.useState<pspWorkspace.HTMLPerspectiveWorkspaceElement>();
63+
64+
React.useImperativeHandle(ref, () => workspace, [workspace]);
65+
66+
React.useEffect(() => {
67+
if (!workspace) return;
68+
workspace.restore(layout);
69+
}, [workspace, layout]);
70+
71+
React.useEffect(() => {
72+
if (!workspace) return;
73+
workspace.replaceTables(tables);
74+
}, [workspace, tables]);
75+
76+
utils.usePspListener(workspace, "workspace-new-view", onNewView);
77+
utils.usePspListener(
78+
workspace,
79+
"workspace-layout-update",
80+
onLayoutUpdate
81+
);
82+
utils.usePspListener(
83+
workspace,
84+
"workspace-toggle-global-filter",
85+
onToggleGlobalFilter
86+
);
87+
88+
return (
89+
<perspective-workspace
90+
ref={(r) => setWorkspace(r ?? undefined)}
91+
id={id}
92+
className={className}
93+
style={style}
94+
></perspective-workspace>
95+
);
96+
}
97+
);
98+
99+
export const PerspectiveWorkspace = React.memo(PerspectiveWorkspaceImpl);

packages/perspective-react/test/js/basic.story.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import perspective from "@finos/perspective";
1414
import perspective_viewer from "@finos/perspective-viewer";
1515
import "@finos/perspective-viewer-datagrid";
1616
import "@finos/perspective-viewer-d3fc";
17+
import "@finos/perspective-workspace";
1718

1819
// @ts-ignore
1920
import SERVER_WASM from "@finos/perspective/dist/wasm/perspective-server.wasm?url";
@@ -26,12 +27,21 @@ await Promise.all([
2627
perspective_viewer.init_client(fetch(CLIENT_WASM)),
2728
]);
2829

29-
import type * as psp from "@finos/perspective";
30+
import * as psp from "@finos/perspective";
3031
import type * as pspViewer from "@finos/perspective-viewer";
3132

3233
// @ts-ignore
3334
import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow?url";
3435

36+
import * as React from "react";
37+
import {
38+
PerspectiveViewer,
39+
PerspectiveWorkspace,
40+
} from "@finos/perspective-react";
41+
42+
import "@finos/perspective-viewer/dist/css/themes.css";
43+
import "./index.css";
44+
3545
const WORKER = await perspective.worker();
3646

3747
async function createNewSuperstoreTable(): Promise<psp.Table> {
@@ -45,12 +55,6 @@ const CONFIG: pspViewer.ViewerConfigUpdate = {
4555
group_by: ["State"],
4656
};
4757

48-
import * as React from "react";
49-
import { PerspectiveViewer } from "@finos/perspective-react";
50-
51-
import "@finos/perspective-viewer/dist/css/themes.css";
52-
import "./index.css";
53-
5458
interface ToolbarState {
5559
mounted: boolean;
5660
table?: Promise<psp.Table>;

packages/perspective-react/test/js/index.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,17 @@ button {
5252
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
5353
"Consolas", "Liberation Mono", monospace;
5454
}
55+
56+
.workspace-container {
57+
display: flex;
58+
flex-direction: column;
59+
60+
.workspace-toolbar {
61+
display: flex;
62+
flex-direction: row;
63+
}
64+
65+
perspective-workspace {
66+
height: 100vh;
67+
}
68+
}

0 commit comments

Comments
 (0)