waveterm/frontend/app/view/preview/preview-directory-utils.tsx
Mike Sawka 77bbf74ef9
fix bug with CodeEditor/monaco model, more preview refactoring (#2353)
the primary purpose of this PR is to fix a showstopper bug in the
CodeEditor component by setting "path" to a stable UUID. the bug was
that it started as empty string, so it created a shared model between
all of the codeeditor components. now each will get their own monaco
model.

also took this opportunity to do more more preview view refactoring,
splitting up code, and more tailwind migrations.
2025-09-15 16:01:29 -07:00

175 lines
5.6 KiB
TypeScript

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { globalStore } from "@/app/store/global";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { fireAndForget, isBlank } from "@/util/util";
import { Column } from "@tanstack/react-table";
import dayjs from "dayjs";
import React from "react";
import { type PreviewModel } from "./preview-model";
export const recursiveError = "recursive flag must be set for directory operations";
export const overwriteError = "set overwrite flag to delete the existing file";
export const mergeError = "set overwrite flag to delete the existing contents or set merge flag to merge the contents";
export const displaySuffixes = {
B: "b",
kB: "k",
MB: "m",
GB: "g",
TB: "t",
KiB: "k",
MiB: "m",
GiB: "g",
TiB: "t",
};
export function getBestUnit(bytes: number, si = false, sigfig = 3): string {
if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "-";
if (bytes === 0) return "0B";
const units = si ? ["kB", "MB", "GB", "TB"] : ["KiB", "MiB", "GiB", "TiB"];
const divisor = si ? 1000 : 1024;
const idx = Math.min(Math.floor(Math.log(bytes) / Math.log(divisor)), units.length);
const unit = idx === 0 ? "B" : units[idx - 1];
const value = bytes / Math.pow(divisor, idx);
return `${parseFloat(value.toPrecision(sigfig))}${displaySuffixes[unit] ?? unit}`;
}
export function getLastModifiedTime(unixMillis: number, column: Column<FileInfo, number>): string {
const fileDatetime = dayjs(new Date(unixMillis));
const nowDatetime = dayjs(new Date());
let datePortion: string;
if (nowDatetime.isSame(fileDatetime, "date")) {
datePortion = "Today";
} else if (nowDatetime.subtract(1, "day").isSame(fileDatetime, "date")) {
datePortion = "Yesterday";
} else {
datePortion = dayjs(fileDatetime).format("M/D/YY");
}
if (column.getSize() > 120) {
return `${datePortion}, ${dayjs(fileDatetime).format("h:mm A")}`;
}
return datePortion;
}
const iconRegex = /^[a-z0-9- ]+$/;
export function isIconValid(icon: string): boolean {
if (isBlank(icon)) {
return false;
}
return icon.match(iconRegex) != null;
}
export function getSortIcon(sortType: string | boolean): React.ReactNode {
switch (sortType) {
case "asc":
return <i className="fa-solid fa-chevron-up dir-table-head-direction"></i>;
case "desc":
return <i className="fa-solid fa-chevron-down dir-table-head-direction"></i>;
default:
return null;
}
}
export function cleanMimetype(input: string): string {
const truncated = input.split(";")[0];
return truncated.trim();
}
export function handleRename(
model: PreviewModel,
path: string,
newPath: string,
isDir: boolean,
recursive: boolean,
setErrorMsg: (msg: ErrorMsg) => void
) {
fireAndForget(async () => {
try {
let srcuri = await model.formatRemoteUri(path, globalStore.get);
if (isDir) {
srcuri += "/";
}
await RpcApi.FileMoveCommand(TabRpcClient, {
srcuri,
desturi: await model.formatRemoteUri(newPath, globalStore.get),
opts: {
recursive,
},
});
} catch (e) {
const errorText = `${e}`;
console.warn(`Rename failed: ${errorText}`);
let errorMsg: ErrorMsg;
if (errorText.includes(recursiveError) && !recursive) {
errorMsg = {
status: "Confirm Rename Directory",
text: "Renaming a directory requires the recursive flag. Proceed?",
level: "warning",
buttons: [
{
text: "Rename Recursively",
onClick: () => handleRename(model, path, newPath, isDir, true, setErrorMsg),
},
],
};
} else {
errorMsg = {
status: "Rename Failed",
text: `${e}`,
};
}
setErrorMsg(errorMsg);
}
model.refreshCallback();
});
}
export function handleFileDelete(
model: PreviewModel,
path: string,
recursive: boolean,
setErrorMsg: (msg: ErrorMsg) => void
) {
fireAndForget(async () => {
const formattedPath = await model.formatRemoteUri(path, globalStore.get);
try {
await RpcApi.FileDeleteCommand(TabRpcClient, {
path: formattedPath,
recursive,
});
} catch (e) {
const errorText = `${e}`;
console.warn(`Delete failed: ${errorText}`);
let errorMsg: ErrorMsg;
if (errorText.includes(recursiveError) && !recursive) {
errorMsg = {
status: "Confirm Delete Directory",
text: "Deleting a directory requires the recursive flag. Proceed?",
level: "warning",
buttons: [
{
text: "Delete Recursively",
onClick: () => handleFileDelete(model, path, true, setErrorMsg),
},
],
};
} else {
errorMsg = {
status: "Delete Failed",
text: `${e}`,
};
}
setErrorMsg(errorMsg);
}
model.refreshCallback();
});
}