Skip to content

Commit e7e87df

Browse files
committed
PerspectiveWorkspace React component
Signed-off-by: Davis Silverman <[email protected]>
1 parent a004bdb commit e7e87df

File tree

29 files changed

+1323
-220
lines changed

29 files changed

+1323
-220
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 esbuild from "esbuild";
14+
import fs from "fs";
15+
import path from "path";
16+
import { fileURLToPath } from "url";
17+
import { dirname } from "path";
18+
19+
const __dirname = dirname(fileURLToPath(import.meta.url));
20+
21+
await esbuild.build({
22+
entryPoints: ["src/index.tsx"],
23+
outdir: "dist",
24+
format: "esm",
25+
bundle: true,
26+
sourcemap: "linked",
27+
target: "es2022",
28+
loader: {
29+
".arrow": "file",
30+
".wasm": "file",
31+
},
32+
assetNames: "[name]",
33+
});
34+
35+
fs.writeFileSync(
36+
path.join(__dirname, "dist/index.html"),
37+
fs.readFileSync(path.join(__dirname, "src/index.html")).toString()
38+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
declare module "*.wasm" {
14+
const content: string;
15+
export default content;
16+
}
17+
18+
declare module "*.arrow" {
19+
const content: string;
20+
export default content;
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "react-workspace-example",
3+
"private": true,
4+
"version": "3.7.4",
5+
"description": "An example app using @finos/perspective-react and its workspace component",
6+
"type": "module",
7+
"scripts": {
8+
"build": "node build.js",
9+
"start": "node build.js && http-server dist"
10+
},
11+
"keywords": [],
12+
"license": "Apache-2.0",
13+
"dependencies": {
14+
"@finos/perspective": "workspace:^",
15+
"@finos/perspective-viewer": "workspace:^",
16+
"@finos/perspective-viewer-d3fc": "workspace:^",
17+
"@finos/perspective-viewer-datagrid": "workspace:^",
18+
"@finos/perspective-react": "workspace:^",
19+
"react": "^18.0.0",
20+
"react-dom": "^18.0.0",
21+
"superstore-arrow": "^3.0.0"
22+
},
23+
"devDependencies": {
24+
"esbuild": "^0.25.5",
25+
"http-server": "^14.1.1",
26+
"@types/react": "^18",
27+
"@types/react-dom": "^18"
28+
}
29+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
14+
body {
15+
margin: 0;
16+
background-color: #f0f0f0;
17+
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
18+
"Consolas", "Liberation Mono", monospace;
19+
}
20+
21+
.container {
22+
position: absolute;
23+
top: 0;
24+
left: 0;
25+
right: 0;
26+
bottom: 0;
27+
28+
display: grid;
29+
gap: 8px;
30+
grid-template-columns: 1fr 1fr;
31+
grid-template-rows: 45px 1fr;
32+
}
33+
34+
perspective-viewer {
35+
grid-row: 2;
36+
}
37+
38+
.toolbar {
39+
grid-row: 1;
40+
grid-column-start: 1;
41+
grid-column-end: 3;
42+
43+
display: flex;
44+
flex-direction: row;
45+
gap: 10px;
46+
padding: 10px;
47+
justify-content: stretch;
48+
border-bottom: 1px solid #666;
49+
}
50+
51+
button {
52+
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
53+
"Consolas", "Liberation Mono", monospace;
54+
}
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+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!--
2+
3+
Copyright (c) 2017, the Perspective Authors.
4+
5+
This file is part of the Perspective library, distributed under the terms of
6+
the Apache License 2.0. The full license can be found in the LICENSE file.
7+
-->
8+
<!DOCTYPE html>
9+
<html>
10+
<head>
11+
<meta charset="UTF-8" />
12+
<title>Perspective React Example</title>
13+
<link rel="stylesheet" href="./index.css" />
14+
<script type="module" src="./index.js"></script>
15+
</head>
16+
<body>
17+
<div id="root"></div>
18+
</body>
19+
</html>
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
// # [Perspective bootstrapping](https://perspective.finos.org/guide/how_to/javascript/importing.html)
14+
15+
// Here we're initializing the WASM interpreter that powers the perspective API
16+
// and viewer, as covered in the [user guide section on bundling](https://perspective.finos.org/guide/how_to/javascript/importing.html).
17+
// This example is written assuming that the bundler is configured
18+
// to treat these files as a "file" and returns a path as the default export.
19+
// Use ./build.js as an example. The type stubs are in ./globals.d.ts
20+
21+
import perspective from "@finos/perspective";
22+
import perspective_viewer from "@finos/perspective-viewer";
23+
import "@finos/perspective-viewer-datagrid";
24+
import "@finos/perspective-viewer-d3fc";
25+
26+
import SERVER_WASM from "@finos/perspective/dist/wasm/perspective-server.wasm";
27+
import CLIENT_WASM from "@finos/perspective-viewer/dist/wasm/perspective-viewer.wasm";
28+
29+
await Promise.all([
30+
perspective.init_server(fetch(SERVER_WASM)),
31+
perspective_viewer.init_client(fetch(CLIENT_WASM)),
32+
]);
33+
34+
// # Data Source
35+
36+
// Data source creates a static Web Worker instance of Perspective engine, and a
37+
// table creation function which both downloads data and loads it into the
38+
// engine.
39+
40+
import type * as psp from "@finos/perspective";
41+
import type * as pspViewer from "@finos/perspective-viewer";
42+
import * as Workspace from "@finos/perspective-workspace";
43+
44+
import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow";
45+
46+
const CLIENT = await perspective.worker();
47+
48+
async function createNewSuperstoreTable(): Promise<psp.Table> {
49+
console.warn("Creating new table!");
50+
const req = fetch(SUPERSTORE_ARROW);
51+
const resp = await req;
52+
const buffer = await resp.arrayBuffer();
53+
return await CLIENT.table(buffer);
54+
}
55+
56+
const CONFIG: pspViewer.ViewerConfigUpdate = {
57+
group_by: ["State"],
58+
};
59+
60+
// # React application
61+
62+
// The React application itself
63+
64+
import * as React from "react";
65+
import { createRoot } from "react-dom/client";
66+
import { PerspectiveWorkspace } from "@finos/perspective-react";
67+
import { PerspectiveWorkspaceConfig } from "@finos/perspective-workspace";
68+
69+
import "@finos/perspective-viewer/dist/css/pro.css";
70+
import "@finos/perspective-workspace/dist/css/pro.css";
71+
import "./index.css";
72+
73+
interface WorkspaceState {
74+
mounted: boolean;
75+
config: PerspectiveWorkspaceConfig<string>;
76+
tables: Record<string, Promise<psp.Table>>;
77+
/// This object is kept for the 'swap tables' button.
78+
/// It is a backup set of tables that correspond in keys to `tables`
79+
/// but with different data.
80+
swapTables: Record<string, Promise<psp.Table>>;
81+
/// if false use `tables` and true use `swapTables` in the workspace
82+
swap: boolean;
83+
}
84+
85+
const WorkspaceApp: React.FC = () => {
86+
const [state, setState] = React.useState<WorkspaceState>({
87+
mounted: true,
88+
tables: {},
89+
swapTables: {},
90+
config: {
91+
sizes: [],
92+
viewers: {},
93+
detail: undefined,
94+
},
95+
swap: false,
96+
});
97+
98+
const onClickAddViewer = React.useCallback(async () => {
99+
const name = window.crypto.randomUUID();
100+
const data = `a,b,c\n${Math.random()},${Math.random()},${Math.random()}`;
101+
const swapData = `a,b,c\n${Math.random()},${Math.random()},${Math.random()}\n${Math.random()},${Math.random()},${Math.random()}`;
102+
// dont assign internal names to the tables they are not used by the workspace
103+
const t = CLIENT.table(data);
104+
const swap = CLIENT.table(swapData);
105+
const config = Workspace.addViewer(state.config, {
106+
table: name,
107+
title: name,
108+
});
109+
const tables = { ...state.tables, [name]: t };
110+
const swapTables = { ...state.swapTables, [name]: swap };
111+
setState({
112+
...state,
113+
tables,
114+
config,
115+
swapTables,
116+
});
117+
}, [state]);
118+
119+
const onLayoutUpdate: (detail: {
120+
layout: PerspectiveWorkspaceConfig<string>;
121+
tables: Record<string, psp.Table | Promise<psp.Table>>;
122+
}) => void = React.useCallback(
123+
({ layout, tables }) => {
124+
const newTables = Object.fromEntries(
125+
Object.entries(tables).map(([k, v]) => [k, Promise.resolve(v)])
126+
);
127+
setState({
128+
...state,
129+
config: layout,
130+
tables: state.swap ? state.tables : newTables,
131+
swapTables: state.swap ? newTables : state.swapTables,
132+
});
133+
},
134+
[state]
135+
);
136+
137+
const onClickToggleMount = () =>
138+
setState((old) => ({ ...old, mounted: !state.mounted }));
139+
140+
// swaps the tables out but uses the same name of them.
141+
// this keeps the layout the same, but the data within each viewer changes
142+
const swapTables = React.useCallback(() => {
143+
setState({
144+
...state,
145+
swap: !state.swap,
146+
})
147+
}, [state]);
148+
149+
return (
150+
<div className="workspace-container">
151+
<div className="workspace-toolbar">
152+
<button className="toggle-mount" onClick={onClickToggleMount}>
153+
Toggle Mount
154+
</button>
155+
<button className="add-viewer" onClick={onClickAddViewer}>
156+
Add Viewer
157+
</button>
158+
<button className="swap" onClick={swapTables}>
159+
Swap underlying tables
160+
</button>
161+
</div>
162+
{state.mounted && (
163+
<PerspectiveWorkspace
164+
tables={state.swap ? state.swapTables : state.tables}
165+
config={state.config}
166+
onLayoutUpdate={onLayoutUpdate}
167+
/>
168+
)}
169+
</div>
170+
);
171+
};
172+
173+
createRoot(document.getElementById("root")!).render(<WorkspaceApp />);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "bundler",
4+
"target": "ESNext",
5+
"module": "ESNext",
6+
"lib": [
7+
"ES2020",
8+
"dom"
9+
],
10+
"strict": true,
11+
"alwaysStrict": true,
12+
"noImplicitAny": true,
13+
"noImplicitReturns": true,
14+
"noImplicitThis": true,
15+
"noImplicitUseStrict": false,
16+
"noUnusedLocals": false,
17+
"strictNullChecks": true,
18+
"skipLibCheck": false,
19+
"removeComments": false,
20+
"jsx": "react-jsx",
21+
"allowSyntheticDefaultImports": true,
22+
"esModuleInterop": true,
23+
"forceConsistentCasingInFileNames": true,
24+
"importHelpers": false,
25+
"noEmitHelpers": true,
26+
"inlineSourceMap": false,
27+
"sourceMap": true,
28+
"emitDecoratorMetadata": false,
29+
"experimentalDecorators": true,
30+
"downlevelIteration": true,
31+
"pretty": true
32+
},
33+
"exclude": [
34+
"node_modules"
35+
]
36+
}

0 commit comments

Comments
 (0)