Restrict interfaces with projects for components

This commit is contained in:
Sam Nolan 2022-10-05 11:57:32 +11:00
parent b512751110
commit bb6fece694
3 changed files with 78 additions and 116 deletions

View File

@ -11,15 +11,13 @@ import { useSquiggle } from "../lib/hooks";
import { SquiggleViewer } from "./SquiggleViewer";
import { JsImports } from "../lib/jsImports";
export interface SquiggleChartProps {
export type SquiggleChartProps = {
/** The input string for squiggle */
code?: string;
code: string;
/** Allows to re-run the code if code hasn't changed */
executionId?: number;
/** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number;
/** The amount of points returned to draw the distribution */
environment?: environment;
/** If the result is a function, where the function domain starts */
diagramStart?: number;
/** If the result is a function, where the function domain ends */
@ -54,50 +52,66 @@ export interface SquiggleChartProps {
/** Whether to show vega actions to the user, so they can copy the chart spec */
distributionChartActions?: boolean;
enableLocalSettings?: boolean;
/** The project that this execution is part of */
project?: SqProject;
/** The name of the squiggle execution source. Generates a UUID if not given */
sourceName?: string;
/** The sources that this execution continues */
includes?: string[];
}
} & (StandaloneExecutionProps | ProjectExecutionProps);
// Props needed for a standalone execution
type StandaloneExecutionProps = {
/** Project must be undefined */
project?: undefined;
/** Includes must be undefined */
includes?: undefined;
/** The amount of points returned to draw the distribution, not needed if using a project */
environment?: environment;
};
// Props needed when executing inside a project.
type ProjectExecutionProps = {
/** environment must be undefined (we don't set it here, users can set the environment outside the execution) */
environment?: undefined;
/** The project that this execution is part of */
project: SqProject;
/** What other squiggle sources from the project to include. Default none */
includes?: string[];
};
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
({
code,
executionId = 0,
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
jsImports = defaultImports,
showSummary = false,
width,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 20,
tickFormat,
minX,
maxX,
color,
title,
xAxisType = "number",
distributionChartActions,
enableLocalSettings = false,
sourceName,
includes = [],
project = SqProject.create(),
}) => {
const { result, bindings } = useSquiggle({
sourceName,
includes,
project,
(props: SquiggleChartProps) => {
const {
executionId = 0,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
jsImports = defaultImports,
showSummary = false,
width,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 20,
tickFormat,
minX,
maxX,
color,
title,
xAxisType = "number",
distributionChartActions,
enableLocalSettings = false,
project = SqProject.create(),
code,
includes = [],
} = props;
const p = project ?? SqProject.create();
if (!project && props.environment) {
p.setEnvironment(props.environment);
}
const { result, bindings } = useSquiggle({
includes,
project: p,
code,
environment,
jsImports,
onChange,
executionId,
@ -133,7 +147,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
environment={p.getEnvironment()}
enableLocalSettings={enableLocalSettings}
/>
);

View File

@ -7,9 +7,7 @@ type SquiggleArgs = {
code?: string;
executionId?: number;
jsImports?: JsImports;
environment?: environment;
project: SqProject;
sourceName?: string;
includes: string[];
onChange?: (expr: SqValue | undefined, sourceName: string) => void;
};
@ -17,32 +15,15 @@ type SquiggleArgs = {
const importSourceName = (sourceName: string) => "imports-" + sourceName;
export const useSquiggle = (args: SquiggleArgs) => {
const autogenName = useMemo(() => uuid.v4(), []);
const sourceName = useMemo(() => uuid.v4(), []);
const result = useMemo(
() => {
const project = args.project;
let needsClean = true;
let sourceName = "";
// If the user specified a source and it already exists, assume we don't
// own the source
if (args.sourceName && project.getSource(args.sourceName)) {
needsClean = false;
sourceName = args.sourceName;
} else {
// Otherwise create a source, either with the name given or an automatic one
if (args.sourceName) {
sourceName = args.sourceName;
} else {
sourceName = autogenName;
}
project.setSource(sourceName, args.code ?? "");
}
project.setSource(sourceName, args.code ?? "");
let includes = args.includes;
if (args.environment) {
project.setEnvironment(args.environment);
}
if (args.jsImports && Object.keys(args.jsImports).length) {
const importsSource = jsImportsToSquiggleCode(args.jsImports);
project.setSource(importSourceName(sourceName), importsSource);
@ -54,8 +35,18 @@ export const useSquiggle = (args: SquiggleArgs) => {
const bindings = project.getBindings(sourceName);
return { result, bindings, sourceName, needsClean };
},
// This complains about executionId not being used inside the function body.
// This is on purpose, as executionId simply allows you to run the squiggle
// code again
// eslint-disable-next-line react-hooks/exhaustive-deps
[args.code, args.environment, args.jsImports, args.executionId]
[
args.code,
args.jsImports,
args.executionId,
sourceName,
args.includes,
args.project,
]
);
const { onChange } = args;
@ -67,17 +58,13 @@ export const useSquiggle = (args: SquiggleArgs) => {
);
}, [result, onChange]);
useEffect(
() => {
return () => {
if (result.needsClean) args.project.removeSource(result.sourceName);
if (args.project.getSource(importSourceName(result.sourceName)))
args.project.removeSource(result.sourceName);
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
return () => {
args.project.removeSource(result.sourceName);
if (args.project.getSource(importSourceName(result.sourceName)))
args.project.removeSource(result.sourceName);
};
}, [args.project, result.sourceName]);
return result;
};

View File

@ -10,6 +10,7 @@ test("Creates and cleans up source with no name", async () => {
const { unmount } = render(
<SquiggleChart code={"normal(0, 1)"} project={project} />
);
expect(project.getSourceIds().length).toBe(1);
const sourceId = project.getSourceIds()[0];
@ -19,43 +20,3 @@ test("Creates and cleans up source with no name", async () => {
expect(project.getSourceIds().length).toBe(0);
expect(project.getSource(sourceId)).toBe(undefined);
});
test("Does not clean up existing source", async () => {
const project = SqProject.create();
project.setSource("main", "normal(0, 1)");
const { unmount } = render(
<SquiggleChart sourceName={"main"} project={project} />
);
expect(project.getSourceIds()).toStrictEqual(["main"]);
const sourceId = project.getSourceIds()[0];
expect(project.getSource(sourceId)).toBe("normal(0, 1)");
unmount();
expect(project.getSourceIds()).toStrictEqual(["main"]);
expect(project.getSource(sourceId)).toBe("normal(0, 1)");
});
test("Does clean up when given non-existant source", async () => {
const project = SqProject.create();
const { unmount } = render(
<SquiggleChart
code={"normal(0, 1)"}
sourceName={"main"}
project={project}
/>
);
expect(project.getSourceIds()).toStrictEqual(["main"]);
const sourceId = project.getSourceIds()[0];
expect(project.getSource(sourceId)).toBe("normal(0, 1)");
unmount();
expect(project.getSourceIds()).toStrictEqual([]);
expect(project.getSource(sourceId)).toBe(undefined);
});