mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-21 21:32:13 +01:00
Merge pull request #5 from wavetermdev/sylvie/preview-dir
Preview Directory
This commit is contained in:
commit
9e7e7af04a
49
frontend/app/element/directorytable.less
Normal file
49
frontend/app/element/directorytable.less
Normal file
@ -0,0 +1,49 @@
|
||||
.dir-table {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
.dir-table-head {
|
||||
.dir-table-head-row {
|
||||
display: flex;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
padding: 4px 0;
|
||||
.dir-table-head-cell {
|
||||
position: relative;
|
||||
padding: 2px 4px;
|
||||
.dir-table-head-resize {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
width: 2px;
|
||||
background: linear-gradient(var(--border-color), var(--border-color)) no-repeat center/2px 100%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.dir-table-head-resize {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dir-table-body {
|
||||
.dir-table-body-row {
|
||||
display: flex;
|
||||
border-radius: 3px;
|
||||
&:focus {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.dir-table-body-cell {
|
||||
overflow: hidden;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
109
frontend/app/element/directorytable.tsx
Normal file
109
frontend/app/element/directorytable.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from "react";
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, Table } from "@tanstack/react-table";
|
||||
import { FileInfo } from "@/bindings/fileservice";
|
||||
|
||||
import "./directorytable.less";
|
||||
|
||||
interface DirectoryTableProps {
|
||||
data: FileInfo[];
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<FileInfo>();
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("path", {
|
||||
cell: (info) => info.getValue(),
|
||||
header: () => <span>Name</span>,
|
||||
}),
|
||||
columnHelper.accessor("size", {
|
||||
cell: (info) => info.getValue(),
|
||||
header: () => <span>Size</span>,
|
||||
}),
|
||||
columnHelper.accessor("mimetype", {
|
||||
cell: (info) => info.getValue(),
|
||||
header: () => <span>Mimetype</span>,
|
||||
}),
|
||||
];
|
||||
|
||||
function DirectoryTable<T, U>({ data }: DirectoryTableProps) {
|
||||
const [columns] = React.useState<typeof defaultColumns>(() => [...defaultColumns]);
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
columnResizeMode: "onChange",
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const columnSizeVars = React.useMemo(() => {
|
||||
const headers = table.getFlatHeaders();
|
||||
const colSizes: { [key: string]: number } = {};
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i]!;
|
||||
colSizes[`--header-${header.id}-size`] = header.getSize();
|
||||
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
||||
}
|
||||
return colSizes;
|
||||
}, [table.getState().columnSizingInfo]);
|
||||
|
||||
return (
|
||||
<div className="dir-table" style={{ ...columnSizeVars }}>
|
||||
<div className="dir-table-head">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<div className="dir-table-head-row" key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<div
|
||||
className="dir-table-head-cell"
|
||||
key={header.id}
|
||||
style={{ width: `calc(var(--header-${header.id}-size) * 1px)` }}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
<div
|
||||
className="dir-table-head-resize"
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{table.getState().columnSizingInfo.isResizingColumn ? (
|
||||
<MemoizedTableBody table={table} />
|
||||
) : (
|
||||
<TableBody table={table} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ table }: { table: Table<FileInfo> }) {
|
||||
return (
|
||||
<div className="dir-table-body">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<div className="dir-table-body-row" key={row.id} tabIndex={0}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<div
|
||||
className="dir-table-body-cell"
|
||||
key={cell.id}
|
||||
style={{ width: `calc(var(--col-${cell.column.id}-size) * 1px)` }}
|
||||
>
|
||||
{cell.renderValue<any>()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedTableBody = React.memo(
|
||||
TableBody,
|
||||
(prev, next) => prev.table.options.data == next.table.options.data
|
||||
) as typeof TableBody;
|
||||
|
||||
export { DirectoryTable };
|
@ -1,3 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.modal-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@/element/button";
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { Markdown } from "@/element/markdown";
|
||||
import { FileService, FileInfo, FullFile } from "@/bindings/fileservice";
|
||||
import * as util from "@/util/util";
|
||||
import { CenteredDiv } from "../element/quickelems";
|
||||
import { DirectoryTable } from "@/element/directorytable";
|
||||
|
||||
import "./view.less";
|
||||
|
||||
@ -53,6 +54,12 @@ function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) {
|
||||
return <CenteredDiv>Preview Not Supported</CenteredDiv>;
|
||||
}
|
||||
|
||||
function DirectoryPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<string>> }) {
|
||||
const contentText = jotai.useAtomValue(contentAtom);
|
||||
let content: FileInfo[] = JSON.parse(contentText);
|
||||
return <DirectoryTable data={content} />;
|
||||
}
|
||||
|
||||
function PreviewView({ blockId }: { blockId: string }) {
|
||||
const blockDataAtom: jotai.Atom<BlockData> = blockDataMap.get(blockId);
|
||||
const fileNameAtom = useBlockAtom(blockId, "preview:filename", () =>
|
||||
@ -118,6 +125,9 @@ function PreviewView({ blockId }: { blockId: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (mimeType === "directory") {
|
||||
return <DirectoryPreview contentAtom={fileContentAtom} />;
|
||||
}
|
||||
return (
|
||||
<div className="view-preview">
|
||||
<div>Preview ({mimeType})</div>
|
||||
|
@ -63,4 +63,8 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
&.view-preview-directory {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +92,9 @@ function Widgets() {
|
||||
<div className="widget" onClick={() => clickPreview("build/appicon.png")}>
|
||||
<i className="fa fa-solid fa-files fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPreview("~")}>
|
||||
<i className="fa fa-solid fa-files fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPlot()}>
|
||||
<i className="fa fa-solid fa-chart-simple fa-fw" />
|
||||
</div>
|
||||
|
@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@observablehq/plot": "^0.6.14",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"base64-js": "^1.5.1",
|
||||
|
@ -5,6 +5,7 @@ package fileservice
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -41,9 +42,9 @@ func (fs *FileService) StatFile(path string) (*FileInfo, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot stat file %q: %w", path, err)
|
||||
}
|
||||
mimeType := utilfn.DetectMimeType(path)
|
||||
mimeType := utilfn.DetectMimeType(cleanedPath)
|
||||
return &FileInfo{
|
||||
Path: wavebase.ReplaceHomeDir(path),
|
||||
Path: cleanedPath,
|
||||
Size: finfo.Size(),
|
||||
Mode: finfo.Mode(),
|
||||
ModTime: finfo.ModTime().UnixMilli(),
|
||||
@ -63,6 +64,35 @@ func (fs *FileService) ReadFile(path string) (*FullFile, error) {
|
||||
if finfo.Size > MaxFileSize {
|
||||
return nil, fmt.Errorf("file %q is too large to read, use /wave/stream-file", path)
|
||||
}
|
||||
if finfo.IsDir {
|
||||
innerFilesEntries, err := os.ReadDir(finfo.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse directory %s", finfo.Path)
|
||||
}
|
||||
|
||||
var innerFilesInfo []FileInfo
|
||||
for _, innerFileEntry := range innerFilesEntries {
|
||||
innerFileInfoInt, _ := innerFileEntry.Info()
|
||||
innerFileInfo := FileInfo{
|
||||
Path: innerFileInfoInt.Name(),
|
||||
Size: innerFileInfoInt.Size(),
|
||||
Mode: innerFileInfoInt.Mode(),
|
||||
ModTime: innerFileInfoInt.ModTime().UnixMilli(),
|
||||
IsDir: innerFileInfoInt.IsDir(),
|
||||
MimeType: "",
|
||||
}
|
||||
innerFilesInfo = append(innerFilesInfo, innerFileInfo)
|
||||
}
|
||||
|
||||
filesSerialized, err := json.Marshal(innerFilesInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize files %s", finfo.Path)
|
||||
}
|
||||
return &FullFile{
|
||||
Info: finfo,
|
||||
Data64: base64.StdEncoding.EncodeToString(filesSerialized),
|
||||
}, nil
|
||||
}
|
||||
cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path))
|
||||
barr, err := os.ReadFile(cleanedPath)
|
||||
if err != nil {
|
||||
|
@ -634,6 +634,13 @@ func DetectMimeType(path string) string {
|
||||
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
||||
return mimeType
|
||||
}
|
||||
stats, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if stats.IsDir() {
|
||||
return "directory"
|
||||
}
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
|
Loading…
Reference in New Issue
Block a user