Moved "extension/ui" to "webview", removed "shared".
This commit is contained in:
108
webview/src/components/ExpressionInput.tsx
Normal file
108
webview/src/components/ExpressionInput.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React = require("react");
|
||||
import { Model } from "../model/Model";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, action, autorun } from "mobx";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
@observer
|
||||
export class ExpressionInput extends React.Component<{ model: Model }> {
|
||||
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
@observable private contentHeight: number | undefined = undefined;
|
||||
private model = monaco.editor.createModel(
|
||||
"",
|
||||
"javascript",
|
||||
monaco.Uri.parse(`file:///main.ts`)
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="component-monaco-editor"
|
||||
style={{
|
||||
// The monaco editor does not have a padding, so we add our own.
|
||||
// We have to match the colors of monaco's background.
|
||||
backgroundColor:
|
||||
this.props.model.theme === "light"
|
||||
? "white"
|
||||
: "#263238",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="part-editor"
|
||||
style={{
|
||||
height: this.contentHeight,
|
||||
}}
|
||||
ref={this.setEditorDiv}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.dispose();
|
||||
}
|
||||
this.model.dispose();
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateText = autorun(() => {
|
||||
const val = this.model.getValue();
|
||||
const newVal = this.props.model.expression;
|
||||
if (val !== newVal) {
|
||||
this.model.setValue(newVal);
|
||||
}
|
||||
});
|
||||
|
||||
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
|
||||
if (!editorDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor = monaco.editor.create(editorDiv, {
|
||||
model: this.model,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: false },
|
||||
fixedOverflowWidgets: true,
|
||||
lineNumbers: "off",
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
automaticLayout: true,
|
||||
scrollbar: {
|
||||
horizontal: "hidden",
|
||||
vertical: "hidden",
|
||||
horizontalScrollbarSize: 0,
|
||||
verticalScrollbarSize: 0,
|
||||
},
|
||||
/*theme: { dark: "vs-dark", light: "vs-light" }[
|
||||
this.props.model.theme
|
||||
],*/
|
||||
});
|
||||
//
|
||||
this.editor.onDidContentSizeChange(e => {
|
||||
this.contentHeight = e.contentHeight;
|
||||
});
|
||||
|
||||
this.editor.onKeyDown(e => {
|
||||
if (e.keyCode == monaco.KeyCode.Enter) {
|
||||
if (
|
||||
(this.model.getLineCount() <= 1 || e.ctrlKey) &&
|
||||
!e.shiftKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@action.bound
|
||||
submit() {
|
||||
const val = this.model.getValue();
|
||||
console.log(val);
|
||||
this.props.model.setExpression(val);
|
||||
}
|
||||
}
|
||||
138
webview/src/components/GUI.tsx
Normal file
138
webview/src/components/GUI.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React = require("react");
|
||||
import { Button, Spinner } from "@blueprintjs/core";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Model } from "../model/Model";
|
||||
import { ExpressionInput } from "./ExpressionInput";
|
||||
import { Visualizer } from "./Visualizer";
|
||||
import classnames = require("classnames");
|
||||
import { VisualizerHeaderDetails } from "./VisualizerHeaderDetails";
|
||||
|
||||
@observer
|
||||
export class GUI extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
return (
|
||||
<div
|
||||
className="component-GUI"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="part-Header">
|
||||
<VisualizerHeader model={m} />
|
||||
</div>
|
||||
<div
|
||||
className="part-Visualizer"
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
>
|
||||
<Visualizer model={m} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class VisualizerHeader extends React.Component<{ model: Model }> {
|
||||
@observable expanded = false;
|
||||
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
return (
|
||||
<div
|
||||
className="component-VisualizerHeader"
|
||||
style={{ display: "flex", flexDirection: "row" }}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
"part-ExpandButton",
|
||||
this.expanded && "expanded"
|
||||
)}
|
||||
onClick={this.toggleExpanded}
|
||||
/>
|
||||
<div
|
||||
className="part-HeaderContent"
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div className="part-HeaderMain">
|
||||
<VisualizerHeaderMain model={m} />
|
||||
</div>
|
||||
|
||||
{this.expanded && (
|
||||
<>
|
||||
<div style={{ height: 6 }} />
|
||||
<VisualizerHeaderDetails model={m} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
toggleExpanded() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
export class VisualizerHeaderMain extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
return (
|
||||
<div
|
||||
className="component-VisualizerHeaderMain"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="part-ExpressionInput "
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
<ExpressionInput model={m} />
|
||||
</div>
|
||||
<div style={{ width: 4 }} />
|
||||
{m.loading ? (
|
||||
<div style={{ padding: "0 4px" }}>
|
||||
<Spinner size={Spinner.SIZE_SMALL} />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-Icon"
|
||||
icon="refresh"
|
||||
onClick={() => m.refresh()}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-Icon"
|
||||
icon="log-in"
|
||||
onClick={() => m.openBrowser()}
|
||||
/>
|
||||
{/*
|
||||
TODO
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-Icon"
|
||||
icon="locate"
|
||||
onClick={() => m.useSelectionAsExpression()}
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
47
webview/src/components/NoData.tsx
Normal file
47
webview/src/components/NoData.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React = require("react");
|
||||
import { observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import Measure from "react-measure";
|
||||
|
||||
@observer
|
||||
export class NoData extends React.Component<{ label: string }> {
|
||||
@observable height = 0;
|
||||
@observable width = 0;
|
||||
|
||||
render() {
|
||||
const { width, height } = this;
|
||||
return (
|
||||
<Measure
|
||||
client={true}
|
||||
onResize={e => {
|
||||
if (e.client) {
|
||||
this.height = e.client.height;
|
||||
this.width = e.client.width;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ measureRef }) => (
|
||||
<div className="component-NoData" ref={measureRef}>
|
||||
<svg>
|
||||
<line x1={0} y1={0} x2={width} y2={height} />
|
||||
<line x1={width} y1={0} x2={0} y2={height} />
|
||||
<rect
|
||||
x={width / 2 - 50}
|
||||
y={height / 2 - 20}
|
||||
width={100}
|
||||
height={40}
|
||||
/>
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height / 2}
|
||||
textAnchor="middle"
|
||||
alignmentBaseline="central"
|
||||
children={this.props.label}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
256
webview/src/components/Point.ts
Normal file
256
webview/src/components/Point.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
function sqr(a: number) {
|
||||
return a * a;
|
||||
}
|
||||
|
||||
export type PointLike =
|
||||
| Point
|
||||
| { x?: number; y: number }
|
||||
| { x: number; y?: number };
|
||||
|
||||
export function point(data: PointLike) {
|
||||
if (data instanceof Point) {
|
||||
return data;
|
||||
}
|
||||
return new Point(data.x || 0, data.y || 0);
|
||||
}
|
||||
|
||||
export class Point {
|
||||
public static readonly Zero = new Point(0, 0);
|
||||
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
|
||||
public mapXY(fn: (arg: number) => number): Point {
|
||||
return new Point(fn(this.x), fn(this.y));
|
||||
}
|
||||
|
||||
public distance(other: PointLike = Point.Zero): number {
|
||||
const d = this.sub(other);
|
||||
return Math.sqrt(sqr(d.x) + sqr(d.y));
|
||||
}
|
||||
|
||||
public sub(other: PointLike): Point {
|
||||
const o = point(other);
|
||||
return new Point(this.x - o.x, this.y - o.y);
|
||||
}
|
||||
|
||||
public add(other: PointLike): Point {
|
||||
const o = point(other);
|
||||
return new Point(this.x + o.x, this.y + o.y);
|
||||
}
|
||||
|
||||
public mul(scalar: number): Point {
|
||||
return new Point(this.x * scalar, this.y * scalar);
|
||||
}
|
||||
|
||||
public div(scalar: number): Point {
|
||||
return new Point(this.x / scalar, this.y / scalar);
|
||||
}
|
||||
|
||||
public equals(other: PointLike) {
|
||||
const o = point(other);
|
||||
return this.x === o.x && this.y === o.y;
|
||||
}
|
||||
|
||||
public angle(): number {
|
||||
const angle = Math.atan2(this.x, this.y);
|
||||
return angle;
|
||||
}
|
||||
|
||||
public getPointCloserTo(dest: PointLike, dist: number): Point {
|
||||
if (this.equals(dest)) return this;
|
||||
|
||||
const angle = point(dest)
|
||||
.sub(this)
|
||||
.angle();
|
||||
|
||||
const result = new Point(
|
||||
this.x + Math.sin(angle) * dist,
|
||||
this.y + Math.cos(angle) * dist
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
public toJson(): { x: number; y: number } {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
}
|
||||
|
||||
function turn(p1: Point, p2: Point, p3: Point): number {
|
||||
const A = (p3.y - p1.y) * (p2.x - p1.x);
|
||||
const B = (p2.y - p1.y) * (p3.x - p1.x);
|
||||
return A > B + Number.MIN_VALUE ? 1 : A + Number.MIN_VALUE < B ? -1 : 0;
|
||||
}
|
||||
|
||||
export function isIntersect(
|
||||
aStart: Point,
|
||||
aEnd: Point,
|
||||
bStart: Point,
|
||||
bEnd: Point
|
||||
): boolean {
|
||||
return (
|
||||
turn(aStart, bStart, bEnd) != turn(aEnd, bStart, bEnd) &&
|
||||
turn(aStart, aEnd, bStart) != turn(aStart, aEnd, bEnd)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liang-Barsky function by Daniel White
|
||||
*
|
||||
* @link http://www.skytopia.com/project/articles/compsci/clipping.html
|
||||
*
|
||||
* @param {number} x0
|
||||
* @param {number} y0
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {array<number>} bbox
|
||||
* @return {array<array<number>>|null}
|
||||
*/
|
||||
function liangBarsky(
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
bbox: [number, number, number, number]
|
||||
): [[number, number], [number, number]] | null {
|
||||
let [xmin, xmax, ymin, ymax] = bbox;
|
||||
let t0 = 0,
|
||||
t1 = 1;
|
||||
let dx = x1 - x0,
|
||||
dy = y1 - y0;
|
||||
let p = 0,
|
||||
q = 0,
|
||||
r = 0;
|
||||
|
||||
for (let edge: 0 | 1 | 2 | 3 = 0; edge < 4; edge++) {
|
||||
// Traverse through left, right, bottom, top edges.
|
||||
if (edge === 0) {
|
||||
p = -dx;
|
||||
q = -(xmin - x0);
|
||||
} else if (edge === 1) {
|
||||
p = dx;
|
||||
q = xmax - x0;
|
||||
} else if (edge === 2) {
|
||||
p = -dy;
|
||||
q = -(ymin - y0);
|
||||
} else if (edge === 3) {
|
||||
p = dy;
|
||||
q = ymax - y0;
|
||||
}
|
||||
|
||||
r = q / p;
|
||||
|
||||
if (p === 0 && q < 0) return null; // Don't draw line at all. (parallel line outside)
|
||||
|
||||
if (p < 0) {
|
||||
if (r > t1) return null;
|
||||
// Don't draw line at all.
|
||||
else if (r > t0) t0 = r; // Line is clipped!
|
||||
} else if (p > 0) {
|
||||
if (r < t0) return null;
|
||||
// Don't draw line at all.
|
||||
else if (r < t1) t1 = r; // Line is clipped!
|
||||
}
|
||||
}
|
||||
|
||||
return [[x0 + t0 * dx, y0 + t0 * dy], [x0 + t1 * dx, y0 + t1 * dy]];
|
||||
}
|
||||
|
||||
export function intersectRectWithLine(
|
||||
start: Point,
|
||||
end: Point,
|
||||
rect: Rectangle
|
||||
): { first: Point; second: Point } | undefined {
|
||||
const r = liangBarsky(start.x, start.y, end.x, end.y, [
|
||||
rect.topLeft.x,
|
||||
rect.bottomRight.x,
|
||||
rect.topLeft.y,
|
||||
rect.bottomRight.y,
|
||||
]);
|
||||
if (!r) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
first: new Point(r[0][0], r[0][1]),
|
||||
second: new Point(r[1][0], r[1][1]),
|
||||
};
|
||||
}
|
||||
|
||||
// first zoom then translate
|
||||
export function scale(
|
||||
clientOffset: Point,
|
||||
clientSize: Point,
|
||||
viewSize: Point
|
||||
): { clientZoom: number; clientOffset: Point } {
|
||||
const clientRatio = clientSize.x / clientSize.y;
|
||||
const viewRatio = viewSize.x / viewSize.y;
|
||||
|
||||
let zoom = 1;
|
||||
|
||||
if (clientRatio < viewRatio) zoom = viewSize.y / clientSize.y;
|
||||
else zoom = viewSize.x / clientSize.x;
|
||||
|
||||
const clientMid = clientOffset.mul(zoom).add(clientSize.mul(zoom / 2));
|
||||
const viewMid = viewSize.div(2);
|
||||
|
||||
const clientOffset2 = viewMid.sub(clientMid);
|
||||
|
||||
return { clientOffset: clientOffset2, clientZoom: zoom };
|
||||
}
|
||||
|
||||
export class Rectangle {
|
||||
public static ofSize(position: PointLike, size: PointLike): Rectangle {
|
||||
const pos = point(position);
|
||||
return new Rectangle(pos, pos.add(size));
|
||||
}
|
||||
|
||||
public static spanning(first: Point, ...rest: Point[]): Rectangle {
|
||||
let min = first.toJson();
|
||||
let max = first.toJson();
|
||||
for (const p of rest) {
|
||||
min.x = Math.min(min.x, p.x);
|
||||
min.y = Math.min(min.y, p.y);
|
||||
max.x = Math.max(max.x, p.x);
|
||||
max.y = Math.max(max.y, p.y);
|
||||
}
|
||||
|
||||
return new Rectangle(point(min), point(max));
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly topLeft: Point,
|
||||
public readonly bottomRight: Point
|
||||
) {}
|
||||
|
||||
get center(): Point {
|
||||
return this.bottomRight.add(this.topLeft).div(2);
|
||||
}
|
||||
|
||||
get size(): Point {
|
||||
return this.bottomRight.sub(this.topLeft);
|
||||
}
|
||||
|
||||
get topRight(): Point {
|
||||
return new Point(this.bottomRight.x, this.topLeft.y);
|
||||
}
|
||||
|
||||
get bottomLeft(): Point {
|
||||
return new Point(this.topLeft.x, this.bottomRight.y);
|
||||
}
|
||||
|
||||
public intersects(selectionRect: Rectangle): boolean {
|
||||
return !(
|
||||
selectionRect.topLeft.x > this.bottomRight.x ||
|
||||
selectionRect.bottomRight.x < this.topLeft.x ||
|
||||
selectionRect.topLeft.y > this.bottomRight.y ||
|
||||
selectionRect.bottomRight.y < this.topLeft.y
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: Rectangle) {
|
||||
return (
|
||||
this.topLeft.equals(other.topLeft) &&
|
||||
this.bottomRight.equals(other.bottomRight)
|
||||
);
|
||||
}
|
||||
}
|
||||
90
webview/src/components/SvgElements.tsx
Normal file
90
webview/src/components/SvgElements.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Point, Rectangle } from "./Point";
|
||||
import * as React from "react";
|
||||
import { Properties } from "csstype";
|
||||
|
||||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
||||
const newObj: Omit<T, K> = {} as any;
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (!keys.includes(key as K)) {
|
||||
(newObj as any)[key] = val;
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
export interface SvgAttributes<T> extends React.DOMAttributes<T> {
|
||||
stroke?: string;
|
||||
className?: string;
|
||||
style?: Properties;
|
||||
pointerEvents?: "none" | "auto" | "initial";
|
||||
}
|
||||
|
||||
export function SvgText(
|
||||
props: {
|
||||
position: Point;
|
||||
children: any;
|
||||
childRef?: React.Ref<SVGTextElement>;
|
||||
textAnchor?: "middle" | "end" | "start";
|
||||
dominantBaseline?: "central" | "middle";
|
||||
} & SvgAttributes<SVGTextElement>
|
||||
) {
|
||||
return (
|
||||
<text
|
||||
x={props.position.x}
|
||||
y={props.position.y}
|
||||
ref={props.childRef}
|
||||
{...omit(props, ["position", "childRef"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgCircle(
|
||||
props: {
|
||||
center: Point;
|
||||
radius: number;
|
||||
childRef?: React.Ref<SVGCircleElement>;
|
||||
} & SvgAttributes<SVGCircleElement>
|
||||
) {
|
||||
return (
|
||||
<circle
|
||||
cx={props.center.x}
|
||||
cy={props.center.y}
|
||||
r={props.radius}
|
||||
ref={props.childRef}
|
||||
{...omit(props, ["center", "radius", "childRef"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgLine(
|
||||
props: { start: Point; end: Point } & SvgAttributes<SVGLineElement>
|
||||
) {
|
||||
return (
|
||||
<line
|
||||
x1={props.start.x}
|
||||
y1={props.start.y}
|
||||
x2={props.end.x}
|
||||
y2={props.end.y}
|
||||
{...omit(props, ["end", "start"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgRect(
|
||||
props: { rectangle: Rectangle; fill?: string } & SvgAttributes<
|
||||
SVGRectElement
|
||||
>
|
||||
) {
|
||||
const r = props.rectangle;
|
||||
return (
|
||||
<rect
|
||||
x={r.topLeft.x}
|
||||
y={r.topLeft.y}
|
||||
width={r.size.x}
|
||||
height={r.size.y}
|
||||
{...omit(props, ["rectangle"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
36
webview/src/components/Visualizer.tsx
Normal file
36
webview/src/components/Visualizer.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React = require("react");
|
||||
import { Model } from "../model/Model";
|
||||
import { observer } from "mobx-react";
|
||||
import { NoData } from "./NoData";
|
||||
|
||||
@observer
|
||||
export class Visualizer extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="component-Visualizer">{this.renderContent()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent(): JSX.Element {
|
||||
const s = this.props.model.state;
|
||||
if (s.kind === "loading") {
|
||||
return <NoData label="Loading" />;
|
||||
} else if (s.kind === "error") {
|
||||
return <NoData label={"Error: " + s.message} />;
|
||||
} else if (s.kind === "noExpression") {
|
||||
return <NoData label="No Expression" />;
|
||||
} else if (s.kind === "noDebugSession") {
|
||||
return <NoData label="No Debug Session" />;
|
||||
} else if (s.kind === "data") {
|
||||
const vis = this.props.model.visualizations;
|
||||
if (!vis || !vis.visualization) {
|
||||
return <NoData label="No Visualization" />;
|
||||
}
|
||||
|
||||
return vis.visualization.render();
|
||||
} else {
|
||||
const nvr: never = s;
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
}
|
||||
151
webview/src/components/VisualizerHeaderDetails.tsx
Normal file
151
webview/src/components/VisualizerHeaderDetails.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React = require("react");
|
||||
import { Popover } from "@blueprintjs/core";
|
||||
import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Model } from "../model/Model";
|
||||
import { VisualizationId } from "../visualizers/Visualizer";
|
||||
|
||||
@observer
|
||||
export class VisualizerHeaderDetails extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
let availableExtractors = new Array<DataExtractorInfo>();
|
||||
let selectedExtractor = -1;
|
||||
if (m.state.kind === "data") {
|
||||
const result = m.state.result;
|
||||
availableExtractors = result.availableExtractors;
|
||||
selectedExtractor = result.availableExtractors.findIndex(
|
||||
i => i.id === result.usedExtractor.id
|
||||
);
|
||||
}
|
||||
|
||||
let visualizations = new Array<{
|
||||
label: string;
|
||||
id: VisualizationId;
|
||||
}>();
|
||||
let selectedVisualization = -1;
|
||||
if (m.visualizations) {
|
||||
visualizations = m.visualizations.allVisualizations.map(v => ({
|
||||
label: v.name,
|
||||
id: v.id,
|
||||
}));
|
||||
if (m.visualizations.visualization) {
|
||||
selectedVisualization = visualizations.findIndex(
|
||||
v => v.id === m.visualizations!.visualization!.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="component-VisualizerHeaderDetails"
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
{/*
|
||||
<NamedSelect
|
||||
name="Source"
|
||||
selected={0}
|
||||
options={[{ label: "js" }]}
|
||||
onSelected={item => {}}
|
||||
/>
|
||||
*/}
|
||||
<div style={{ width: 10 }} />
|
||||
|
||||
<NamedSelect
|
||||
name="Extractor"
|
||||
selected={selectedExtractor}
|
||||
options={availableExtractors.map(e => ({
|
||||
label: e.name,
|
||||
id: e.id,
|
||||
}))}
|
||||
onSelected={item => {
|
||||
m.setPreferredExtractorId(item.id);
|
||||
}}
|
||||
/>
|
||||
<div style={{ width: 10 }} />
|
||||
|
||||
<NamedSelect
|
||||
name="Visualizer"
|
||||
selected={selectedVisualization}
|
||||
onSelected={item => {
|
||||
m.setPreferredVisualizationId(item.id);
|
||||
}}
|
||||
options={
|
||||
m.visualizations
|
||||
? m.visualizations.allVisualizations.map(v => ({
|
||||
label: v.name,
|
||||
id: v.id,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function NamedSelect<T extends SelectableItem>(props: {
|
||||
name: string;
|
||||
selected: number;
|
||||
onSelected: (item: T) => void;
|
||||
options: T[];
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="component-NamedSelect"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="part-Select" style={{ minWidth: 100 }}>
|
||||
<Select {...props} />
|
||||
</div>
|
||||
<div className="part-Name">{props.name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectableItem {
|
||||
label: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
class Select<T extends SelectableItem> extends React.Component<{
|
||||
selected: number;
|
||||
onSelected: (item: T) => void;
|
||||
options: T[];
|
||||
}> {
|
||||
@computed get selected(): T | undefined {
|
||||
return this.props.options[this.props.selected];
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="component-Select">
|
||||
<Popover
|
||||
position="auto-start"
|
||||
modifiers={{ arrow: { enabled: false } }}
|
||||
usePortal={false}
|
||||
>
|
||||
<button className="part-Button">
|
||||
{this.selected ? this.selected.label : "(none)"}
|
||||
</button>
|
||||
<div className="part-Items">
|
||||
{this.props.options.map((o, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="part-Item"
|
||||
onClick={() => this.props.onSelected(o)}
|
||||
>
|
||||
{o.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user