Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 99 additions & 26 deletions playground/src/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { PlayFilledAlt, DocumentDownload } from "@carbon/icons-react";
import { PlayFilledAlt, DocumentDownload, Settings } from "@carbon/icons-react";
import { DataSource, SelectLayoutParams } from "@foxglove/embed";
import { FoxgloveViewer, FoxgloveViewerInterface } from "@foxglove/embed-react";
import { Button, GlobalStyles, IconButton, Tooltip, Typography } from "@mui/material";
import {
Badge,
Button,
GlobalStyles,
IconButton,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Allotment } from "allotment";
import { useCallback, useEffect, useRef, useState } from "react";
import toast, { Toaster } from "react-hot-toast";
Expand All @@ -24,7 +32,7 @@ const useStyles = tss.create(({ theme }) => ({
display: "flex",
// Match the height of the app bar in the Foxglove app
height: "44px",
padding: "0 8px 0 16px",
padding: theme.spacing(0, 1, 0, 2),
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
Expand All @@ -33,6 +41,10 @@ const useStyles = tss.create(({ theme }) => ({
color: theme.palette.text.primary,
container: "topBar / inline-size",
},
settings: {
padding: theme.spacing(2),
borderBottom: `1px solid ${theme.palette.divider}`,
},
title: {
"@container topBar (width < 480px)": {
display: "none",
Expand Down Expand Up @@ -72,6 +84,7 @@ export function Playground(): React.JSX.Element {
const runnerRef = useRef<Runner>(undefined);
const editorRef = useRef<EditorInterface>(null);
const viewerRef = useRef<FoxgloveViewerInterface>(null);
const settingsButtonRef = useRef<HTMLButtonElement>(null);
const { cx, classes } = useStyles();

const [initialState] = useState(() => {
Expand All @@ -95,11 +108,20 @@ export function Playground(): React.JSX.Element {
force: false,
},
);
const [embedURL, setEmbedURL] = useState<URL | undefined>(initialState?.embedURL);
const [embedURLError, setEmbedURLError] = useState<string | undefined>();
const [ready, setReady] = useState(false);
const [mcapFilename, setMcapFilename] = useState<string | undefined>();
const [dataSource, setDataSource] = useState<DataSource | undefined>();
const layoutInputRef = useRef<HTMLInputElement>(null);

const hasModifiedSettings = embedURL != undefined;

const [settingsOpen, setSettingsOpen] = useState(false);
const toggleSettings = useCallback(() => {
setSettingsOpen((open) => !open);
}, []);

useEffect(() => {
setReady(false);
const runner = new Runner();
Expand Down Expand Up @@ -150,39 +172,47 @@ export function Playground(): React.JSX.Element {
setAndCopyUrlState({
code: editor.getValue(),
layout: layout ?? selectedLayout.opaqueLayout,
embedURL,
});
})
.catch((err: unknown) => {
toast.error(`Sharing failed: ${String(err)}`);
});
}, [selectedLayout]);
}, [selectedLayout, embedURL]);

const chooseLayout = useCallback(() => {
layoutInputRef.current?.click();
}, []);

const onLayoutSelected = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
file
.text()
.then(JSON.parse)
.then(
(layout) => {
setSelectedLayout({
storageKey: LAYOUT_STORAGE_KEY,
opaqueLayout: layout,
force: true,
});
setAndCopyUrlState({ code: editorRef.current?.getValue() ?? "", layout });
},
(err: unknown) => {
toast.error(`Failed to load layout: ${String(err)}`);
},
);
}, []);
const onLayoutSelected = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
file
.text()
.then(JSON.parse)
.then(
(layout) => {
setSelectedLayout({
storageKey: LAYOUT_STORAGE_KEY,
opaqueLayout: layout,
force: true,
});
setAndCopyUrlState({
code: editorRef.current?.getValue() ?? "",
layout,
embedURL,
});
},
(err: unknown) => {
toast.error(`Failed to load layout: ${String(err)}`);
},
);
},
[embedURL],
);

const download = useCallback(async () => {
const runner = runnerRef.current;
Expand Down Expand Up @@ -263,8 +293,50 @@ export function Playground(): React.JSX.Element {
<Button variant="outlined" onClick={share}>
Share
</Button>
<IconButton
ref={settingsButtonRef}
onClick={toggleSettings}
color={settingsOpen ? "primary" : "default"}
>
<Badge color="primary" variant="dot" invisible={!hasModifiedSettings}>
<Settings />
</Badge>
</IconButton>
</div>
</div>
{settingsOpen && (
<div className={classes.settings}>
<TextField
label="Embed URL"
defaultValue={embedURL}
placeholder="https://embed.foxglove.dev"
onChange={(event) => {
try {
if (event.target.value) {
new URL(event.target.value);
}
setEmbedURLError(undefined);
} catch (_err) {
setEmbedURLError("Invalid URL");
}
}}
onBlur={(event) => {
try {
if (event.target.value) {
setEmbedURL(new URL(event.target.value));
} else {
setEmbedURL(undefined);
}
setEmbedURLError(undefined);
} catch (_err) {
setEmbedURLError("Invalid URL");
}
}}
error={!!embedURLError}
helperText={embedURLError}
/>
</div>
)}
<Editor
ref={editorRef}
initialValue={initialState?.code ?? DEFAULT_CODE}
Expand All @@ -275,6 +347,7 @@ export function Playground(): React.JSX.Element {
<Allotment.Pane minSize={200}>
<FoxgloveViewer
ref={viewerRef}
src={embedURL?.href}
style={{ width: "100%", height: "100%", overflow: "hidden" }}
data={dataSource}
layout={selectedLayout}
Expand Down
11 changes: 11 additions & 0 deletions playground/src/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ export function ThemeProvider(props: React.PropsWithChildren): React.JSX.Element
disableElevation: true,
},
},
MuiTextField: {
defaultProps: {
variant: "standard",
},
},
MuiInputLabel: {
defaultProps: {
// always position label above text field
shrink: true,
},
},
},
}),
[isDark],
Expand Down
20 changes: 18 additions & 2 deletions playground/src/urlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import zstd from "@foxglove/wasm-zstd";
export type UrlState = {
code: string;
layout?: unknown;
embedURL?: URL;
};

// https://developer.mozilla.org/en-US/docs/Glossary/Base64#url_and_filename_safe_base64
Expand Down Expand Up @@ -52,6 +53,9 @@ function serializeState(state: UrlState): string {
if (state.layout != undefined) {
params.set("layout", compressEncode(JSON.stringify(state.layout)));
}
if (state.embedURL != undefined) {
params.set("embed", state.embedURL.href);
}
return params.toString();
}

Expand All @@ -67,11 +71,23 @@ function deserializeState(serialized: string): UrlState | undefined {
throw new Error(`Unable to decode URL state: missing version ${params.get("v") ?? ""}`);
}
const encodedCode = params.get("code") ?? "";
const encodedLayout = params.get("layout") ?? "";
const code = uncompressDecode(encodedCode);

const encodedLayout = params.get("layout") ?? "";
const layoutJson = uncompressDecode(encodedLayout);
const layout = layoutJson ? (JSON.parse(layoutJson) as unknown) : undefined;
return { code, layout };

let embedURL: URL | undefined;
try {
const embedStr = params.get("embed");
if (embedStr) {
embedURL = new URL(embedStr);
}
} catch {
// ignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not throw here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO if one of the url params is invalid, I'm not sure we should completely ignore all the other ones

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we at least show an error toast or something if the embed URL is an invalid URL?

}

return { code, layout, embedURL };
}

export function getUrlState(): UrlState | undefined {
Expand Down