diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx
index 3a99550e7..fe7af24b2 100644
--- a/frontend/app/block/block.tsx
+++ b/frontend/app/block/block.tsx
@@ -8,7 +8,7 @@ import { atoms, blockDataMap, removeBlockFromTab } from "@/store/global";
import { TerminalView } from "@/app/view/term";
import { PreviewView } from "@/app/view/preview";
import { PlotView } from "@/app/view/plotview";
-import { CenteredLoadingDiv } from "@/element/quickelems";
+import { CenteredDiv } from "@/element/quickelems";
import "./block.less";
@@ -53,7 +53,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => {
- }>{blockElem}
+ Loading...}>{blockElem}
);
diff --git a/frontend/app/element/quickelems.less b/frontend/app/element/quickelems.less
index c443f31c8..b4f845323 100644
--- a/frontend/app/element/quickelems.less
+++ b/frontend/app/element/quickelems.less
@@ -1,7 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
-.centered-loading {
+.centered-div {
display: flex;
flex-direction: row;
align-items: center;
diff --git a/frontend/app/element/quickelems.tsx b/frontend/app/element/quickelems.tsx
index 9717ee434..5de52a253 100644
--- a/frontend/app/element/quickelems.tsx
+++ b/frontend/app/element/quickelems.tsx
@@ -3,12 +3,12 @@
import "./quickelems.less";
-function CenteredLoadingDiv() {
+function CenteredDiv({ children }: { children: React.ReactNode }) {
return (
-
-
Loading...
+
);
}
-export { CenteredLoadingDiv };
+export { CenteredDiv as CenteredDiv };
diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx
index bb8c1c72e..6533fafc1 100644
--- a/frontend/app/view/preview.tsx
+++ b/frontend/app/view/preview.tsx
@@ -7,25 +7,69 @@ import { atoms, blockDataMap, useBlockAtom } from "@/store/global";
import { Markdown } from "@/element/markdown";
import * as FileService from "@/bindings/pkg/service/fileservice/FileService";
import * as util from "@/util/util";
-import { loadable } from "jotai/utils";
+import { CenteredDiv } from "../element/quickelems";
import "./view.less";
-const MarkdownPreview = ({ contentAtom }: { contentAtom: jotai.Atom
> }) => {
+const MaxFileSize = 1024 * 1024 * 10; // 10MB
+
+function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom> }) {
const readmeText = jotai.useAtomValue(contentAtom);
return (
);
-};
-const PreviewView = ({ blockId }: { blockId: string }) => {
+}
+
+function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) {
+ const filePath = fileInfo.path;
+ const streamingUrl = "/wave/stream-file?path=" + encodeURIComponent(filePath);
+ if (fileInfo.mimetype.startsWith("video/")) {
+ return (
+
+
+
+ );
+ }
+ if (fileInfo.mimetype.startsWith("audio/")) {
+ return (
+
+ );
+ }
+ if (fileInfo.mimetype.startsWith("image/")) {
+ return (
+
+

+
+ );
+ }
+ return Preview Not Supported;
+}
+
+function PreviewView({ blockId }: { blockId: string }) {
const blockDataAtom: jotai.Atom = blockDataMap.get(blockId);
const fileNameAtom = useBlockAtom(blockId, "preview:filename", () =>
jotai.atom((get) => {
return get(blockDataAtom)?.meta?.file;
})
);
+ const statFileAtom = useBlockAtom(blockId, "preview:statfile", () =>
+ jotai.atom>(async (get) => {
+ const fileName = get(fileNameAtom);
+ if (fileName == null) {
+ return null;
+ }
+ const statFile = await FileService.StatFile(fileName);
+ return statFile;
+ })
+ );
const fullFileAtom = useBlockAtom(blockId, "preview:fullfile", () =>
jotai.atom>(async (get) => {
const fileName = get(fileNameAtom);
@@ -38,8 +82,8 @@ const PreviewView = ({ blockId }: { blockId: string }) => {
);
const fileMimeTypeAtom = useBlockAtom(blockId, "preview:mimetype", () =>
jotai.atom>(async (get) => {
- const fullFile = await get(fullFileAtom);
- return fullFile?.info?.mimetype;
+ const fileInfo = await get(statFileAtom);
+ return fileInfo?.mimetype;
})
);
const fileContentAtom = useBlockAtom(blockId, "preview:filecontent", () =>
@@ -52,6 +96,18 @@ const PreviewView = ({ blockId }: { blockId: string }) => {
if (mimeType == null) {
mimeType = "";
}
+ const fileInfo = jotai.useAtomValue(statFileAtom);
+
+ // handle streaming files here
+ if (mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType.startsWith("image/")) {
+ return ;
+ }
+ if (fileInfo == null) {
+ return File Not Found;
+ }
+ if (fileInfo.size > MaxFileSize) {
+ return File Too Large to Preview;
+ }
if (mimeType === "text/markdown") {
return ;
}
@@ -67,6 +123,6 @@ const PreviewView = ({ blockId }: { blockId: string }) => {
Preview ({mimeType})
);
-};
+}
export { PreviewView };
diff --git a/frontend/app/view/view.less b/frontend/app/view/view.less
index abb45a3d6..3700d3475 100644
--- a/frontend/app/view/view.less
+++ b/frontend/app/view/view.less
@@ -51,4 +51,16 @@
font: var(--fixed-font);
}
}
+
+ &.view-preview-image,
+ &.view-preview-video,
+ &.view-preview-audio {
+ video,
+ audio,
+ img {
+ width: 100%;
+ padding: 10px;
+ object-fit: contain;
+ }
+ }
}
diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx
index cbc15b8f4..2d03b7942 100644
--- a/frontend/app/workspace/workspace.tsx
+++ b/frontend/app/workspace/workspace.tsx
@@ -89,6 +89,9 @@ function Widgets() {
clickPreview("go.mod")}>
+ clickPreview("build/appicon.png")}>
+
+
clickPlot()}>
diff --git a/main.go b/main.go
index 0ad8df88e..18678152e 100644
--- a/main.go
+++ b/main.go
@@ -8,6 +8,8 @@ package main
import (
"embed"
"log"
+ "net/http"
+ "strings"
"github.com/wavetermdev/thenextwave/pkg/blockstore"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
@@ -61,6 +63,28 @@ func createWindow(app *application.App) {
})
}
+type waveAssetHandler struct {
+ AssetHandler http.Handler
+}
+
+func serveWaveUrls(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/wave/stream-file" {
+ fileName := r.URL.Query().Get("path")
+ fileName = wavebase.ExpandHomeDir(fileName)
+ http.ServeFile(w, r, fileName)
+ return
+ }
+ http.NotFound(w, r)
+}
+
+func (wah waveAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "GET" && strings.HasPrefix(r.URL.Path, "/wave/") {
+ serveWaveUrls(w, r)
+ return
+ }
+ wah.AssetHandler.ServeHTTP(w, r)
+}
+
func main() {
err := wavebase.EnsureWaveHomeDir()
if err != nil {
@@ -83,7 +107,7 @@ func main() {
},
Icon: appIcon,
Assets: application.AssetOptions{
- Handler: application.AssetFileServerFS(assets),
+ Handler: waveAssetHandler{AssetHandler: application.AssetFileServerFS(assets)},
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
diff --git a/pkg/service/fileservice/fileservice.go b/pkg/service/fileservice/fileservice.go
index 306d2465d..a1c486f16 100644
--- a/pkg/service/fileservice/fileservice.go
+++ b/pkg/service/fileservice/fileservice.go
@@ -13,6 +13,8 @@ import (
"github.com/wavetermdev/thenextwave/pkg/wavebase"
)
+const MaxFileSize = 10 * 1024 * 1024 // 10M
+
type FileService struct{}
type FileInfo struct {
@@ -58,6 +60,9 @@ func (fs *FileService) ReadFile(path string) (*FullFile, error) {
if finfo.NotFound {
return &FullFile{Info: finfo}, nil
}
+ if finfo.Size > MaxFileSize {
+ return nil, fmt.Errorf("file %q is too large to read, use /wave/stream-file", path)
+ }
cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path))
barr, err := os.ReadFile(cleanedPath)
if err != nil {