optimize: add routes
This commit is contained in:
@@ -39,6 +39,8 @@ import Icon from "../../components/Icon";
|
|||||||
import SchedulePage from "../schedules/SchedulePage";
|
import SchedulePage from "../schedules/SchedulePage";
|
||||||
import { DashboardPage, SystemAdminPage } from "../admin/AdminPages";
|
import { DashboardPage, SystemAdminPage } from "../admin/AdminPages";
|
||||||
import "../../styles/platform.css";
|
import "../../styles/platform.css";
|
||||||
|
import { WorkspaceTreeGroup, scriptIcon } from "./WorkspaceTree";
|
||||||
|
import { ScriptWorkspace } from "./ScriptWorkspace";
|
||||||
|
|
||||||
|
|
||||||
type NewScriptForm = {
|
type NewScriptForm = {
|
||||||
@@ -88,29 +90,6 @@ const initialForm: NewScriptForm = {
|
|||||||
parentPath: "",
|
parentPath: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatTime(value: string) {
|
|
||||||
return new Intl.DateTimeFormat("zh-CN", {
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: false,
|
|
||||||
}).format(new Date(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBytes(value: number) {
|
|
||||||
if (value < 1024) return `${value} B`;
|
|
||||||
return `${(value / 1024).toFixed(1)} KB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shortHash(value: string) {
|
|
||||||
return value ? `${value.slice(0, 8)}…${value.slice(-6)}` : "—";
|
|
||||||
}
|
|
||||||
|
|
||||||
function scriptIcon(item: ScriptItem) {
|
|
||||||
return item.script_type === "notebook" ? "notebook" : "python";
|
|
||||||
}
|
|
||||||
|
|
||||||
function ownedScriptPath(item: ScriptItem) {
|
function ownedScriptPath(item: ScriptItem) {
|
||||||
return item.relative_path.replaceAll("\\", "/").split("/").slice(2).join("/");
|
return item.relative_path.replaceAll("\\", "/").split("/").slice(2).join("/");
|
||||||
}
|
}
|
||||||
@@ -143,45 +122,6 @@ function mergeDirectories(
|
|||||||
[...left, ...right].map((item) => [item.path, item]),
|
[...left, ...right].map((item) => [item.path, item]),
|
||||||
).values()];
|
).values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
function confineJupyterFrame(frame: HTMLIFrameElement): void {
|
|
||||||
try {
|
|
||||||
const document = frame.contentDocument;
|
|
||||||
if (!document?.documentElement) return;
|
|
||||||
const keepInside = (): void => {
|
|
||||||
document.querySelectorAll<HTMLElement>("button,[role='button']").forEach(
|
|
||||||
(element) => {
|
|
||||||
const label = `${element.getAttribute("aria-label") ?? ""} ${
|
|
||||||
element.getAttribute("title") ?? ""
|
|
||||||
} ${element.textContent ?? ""}`.trim();
|
|
||||||
if (/^open in\b/i.test(label) || /\bopen in\.\.\./i.test(label)) {
|
|
||||||
element.style.setProperty("display", "none", "important");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
document.querySelectorAll<HTMLAnchorElement>("a[target]").forEach((link) => {
|
|
||||||
if (["_blank", "_top", "_parent"].includes(link.target)) {
|
|
||||||
link.target = "_self";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
keepInside();
|
|
||||||
new MutationObserver(keepInside).observe(document.documentElement, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
document.addEventListener("click", (event) => {
|
|
||||||
const target = event.target as HTMLElement | null;
|
|
||||||
const link = target?.closest?.("a") as HTMLAnchorElement | null;
|
|
||||||
if (link && ["_blank", "_top", "_parent"].includes(link.target)) {
|
|
||||||
link.target = "_self";
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
} catch {
|
|
||||||
// The iframe remains sandboxed even if its document is not yet accessible.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ModelPlatformApp() {
|
export default function ModelPlatformApp() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -1519,447 +1459,3 @@ export default function ModelPlatformApp() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceTreeGroup({
|
|
||||||
title,
|
|
||||||
scripts,
|
|
||||||
directories,
|
|
||||||
selectedId,
|
|
||||||
onSelect,
|
|
||||||
onContextMenu,
|
|
||||||
readOnly = false,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
scripts: ScriptItem[];
|
|
||||||
directories: WorkspaceDirectory[];
|
|
||||||
selectedId: string | null;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
onContextMenu?: (
|
|
||||||
event: ReactMouseEvent,
|
|
||||||
target: Omit<ContextMenuState, "x" | "y">,
|
|
||||||
) => void;
|
|
||||||
readOnly?: boolean;
|
|
||||||
}) {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
return (
|
|
||||||
<div className="tree-group">
|
|
||||||
<button
|
|
||||||
className={`tree-group__title${open ? " is-open" : ""}`}
|
|
||||||
type="button"
|
|
||||||
aria-expanded={open}
|
|
||||||
onClick={() => setOpen((current) => !current)}
|
|
||||||
onContextMenu={onContextMenu
|
|
||||||
? (event) => onContextMenu(event, { kind: "root", path: "" })
|
|
||||||
: undefined}
|
|
||||||
>
|
|
||||||
<Icon name="chevron" size={14} />
|
|
||||||
<Icon name="folder" size={17} />
|
|
||||||
<span>{title}</span>
|
|
||||||
<em>{scripts.length}</em>
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<div className="tree-group__items">
|
|
||||||
<WorkspaceTreeItems
|
|
||||||
path=""
|
|
||||||
depth={0}
|
|
||||||
scripts={scripts}
|
|
||||||
directories={directories}
|
|
||||||
selectedId={selectedId}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onContextMenu={onContextMenu}
|
|
||||||
/>
|
|
||||||
{scripts.length === 0 && directories.length === 0 && (
|
|
||||||
<p className="tree-group__empty">
|
|
||||||
{readOnly ? "暂无文件" : "右键此目录即可新建或上传"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspaceTreeItems({
|
|
||||||
path,
|
|
||||||
depth,
|
|
||||||
scripts,
|
|
||||||
directories,
|
|
||||||
selectedId,
|
|
||||||
onSelect,
|
|
||||||
onContextMenu,
|
|
||||||
}: {
|
|
||||||
path: string;
|
|
||||||
depth: number;
|
|
||||||
scripts: ScriptItem[];
|
|
||||||
directories: WorkspaceDirectory[];
|
|
||||||
selectedId: string | null;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
onContextMenu?: (
|
|
||||||
event: ReactMouseEvent,
|
|
||||||
target: Omit<ContextMenuState, "x" | "y">,
|
|
||||||
) => void;
|
|
||||||
}) {
|
|
||||||
const childDirectories = directories.filter(
|
|
||||||
(item) => item.parent_path === path,
|
|
||||||
);
|
|
||||||
const childScripts = scripts.filter(
|
|
||||||
(item) => parentOf(ownedScriptPath(item)) === path,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{childDirectories.map((directory) => (
|
|
||||||
<DirectoryBranch
|
|
||||||
key={directory.path}
|
|
||||||
directory={directory}
|
|
||||||
depth={depth}
|
|
||||||
scripts={scripts}
|
|
||||||
directories={directories}
|
|
||||||
selectedId={selectedId}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onContextMenu={onContextMenu}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{childScripts.map((item) => (
|
|
||||||
<button
|
|
||||||
className={`script-row${
|
|
||||||
selectedId === item.script_id ? " script-row--active" : ""
|
|
||||||
}`}
|
|
||||||
style={{ paddingLeft: 20 + depth * 16 }}
|
|
||||||
key={item.script_id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelect(item.script_id)}
|
|
||||||
onContextMenu={onContextMenu
|
|
||||||
? (event) => onContextMenu(event, {
|
|
||||||
kind: "file",
|
|
||||||
path: ownedScriptPath(item),
|
|
||||||
script: item,
|
|
||||||
})
|
|
||||||
: undefined}
|
|
||||||
>
|
|
||||||
<span className={`file-icon file-icon--${item.script_type}`}>
|
|
||||||
<Icon name={scriptIcon(item)} size={17} />
|
|
||||||
</span>
|
|
||||||
<span className="script-row__copy">
|
|
||||||
<strong title={item.script_name}>{item.script_name}</strong>
|
|
||||||
<small>{formatTime(item.updated_at)}</small>
|
|
||||||
</span>
|
|
||||||
{item.visibility !== "private" && (
|
|
||||||
<span className="visibility-dot" title="Workspace 可见" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DirectoryBranch({
|
|
||||||
directory,
|
|
||||||
depth,
|
|
||||||
scripts,
|
|
||||||
directories,
|
|
||||||
selectedId,
|
|
||||||
onSelect,
|
|
||||||
onContextMenu,
|
|
||||||
}: {
|
|
||||||
directory: WorkspaceDirectory;
|
|
||||||
depth: number;
|
|
||||||
scripts: ScriptItem[];
|
|
||||||
directories: WorkspaceDirectory[];
|
|
||||||
selectedId: string | null;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
onContextMenu?: (
|
|
||||||
event: ReactMouseEvent,
|
|
||||||
target: Omit<ContextMenuState, "x" | "y">,
|
|
||||||
) => void;
|
|
||||||
}) {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
return (
|
|
||||||
<div className="directory-branch">
|
|
||||||
<button
|
|
||||||
className="directory-row"
|
|
||||||
style={{ paddingLeft: 10 + depth * 16 }}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setOpen((current) => !current)}
|
|
||||||
onContextMenu={onContextMenu
|
|
||||||
? (event) => onContextMenu(event, {
|
|
||||||
kind: "directory",
|
|
||||||
path: directory.path,
|
|
||||||
})
|
|
||||||
: undefined}
|
|
||||||
>
|
|
||||||
<span className={`directory-row__chevron${open ? " is-open" : ""}`}>
|
|
||||||
<Icon name="chevron" size={13} />
|
|
||||||
</span>
|
|
||||||
<Icon name="folder" size={17} />
|
|
||||||
<strong title={directory.path}>{directory.name}</strong>
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<WorkspaceTreeItems
|
|
||||||
path={directory.path}
|
|
||||||
depth={depth + 1}
|
|
||||||
scripts={scripts}
|
|
||||||
directories={directories}
|
|
||||||
selectedId={selectedId}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onContextMenu={onContextMenu}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ScriptWorkspace({
|
|
||||||
script,
|
|
||||||
editSession,
|
|
||||||
jupyterUrl,
|
|
||||||
editBusy,
|
|
||||||
openError,
|
|
||||||
latestVersion,
|
|
||||||
versionsLoading,
|
|
||||||
onOpenEditor,
|
|
||||||
onEndEditing,
|
|
||||||
onClose,
|
|
||||||
onPublish,
|
|
||||||
onInfo,
|
|
||||||
}: {
|
|
||||||
script: ScriptItem;
|
|
||||||
editSession: ActiveEditSession | null;
|
|
||||||
jupyterUrl: string | null;
|
|
||||||
editBusy: boolean;
|
|
||||||
openError: string | null;
|
|
||||||
latestVersion: StableVersion | null;
|
|
||||||
versionsLoading: boolean;
|
|
||||||
onOpenEditor: () => void;
|
|
||||||
onEndEditing: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
onPublish: () => void;
|
|
||||||
onInfo: (toast: ToastState) => void;
|
|
||||||
}) {
|
|
||||||
const isNotebook = script.script_type === "notebook";
|
|
||||||
const isEditing = editSession?.session_status === "active";
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="tabbar">
|
|
||||||
<div className="editor-tab editor-tab--active">
|
|
||||||
<span className={`file-icon file-icon--${script.script_type}`}>
|
|
||||||
<Icon name={scriptIcon(script)} size={16} />
|
|
||||||
</span>
|
|
||||||
<span>{script.script_name}</span>
|
|
||||||
<button type="button" aria-label="关闭标签" onClick={onClose}>
|
|
||||||
<Icon name="close" size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="new-tab"
|
|
||||||
type="button"
|
|
||||||
onClick={() => onInfo({ tone: "info", message: "请从左侧选择或新建脚本" })}
|
|
||||||
>
|
|
||||||
<Icon name="plus" size={17} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="editor-toolbar">
|
|
||||||
<div className="editor-toolbar__path">
|
|
||||||
<span className={`file-icon file-icon--${script.script_type}`}>
|
|
||||||
<Icon name={scriptIcon(script)} size={17} />
|
|
||||||
</span>
|
|
||||||
<span>工作副本</span>
|
|
||||||
<Icon name="chevron" size={13} />
|
|
||||||
<strong>{script.script_name}</strong>
|
|
||||||
</div>
|
|
||||||
<div className="editor-toolbar__actions">
|
|
||||||
{isEditing ? (
|
|
||||||
<button
|
|
||||||
className="end-edit-button"
|
|
||||||
type="button"
|
|
||||||
disabled={editBusy}
|
|
||||||
onClick={onEndEditing}
|
|
||||||
>
|
|
||||||
{editBusy ? "正在释放…" : "结束编辑"}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onInfo({
|
|
||||||
tone: "info",
|
|
||||||
message: "Jupyter 中保存后会直接写入 Workspace 工作副本",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
保存说明
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="release-button"
|
|
||||||
type="button"
|
|
||||||
onClick={onPublish}
|
|
||||||
>
|
|
||||||
发布稳定版
|
|
||||||
</button>
|
|
||||||
<span className={`stage-badge${isEditing ? " is-editing" : ""}`}>
|
|
||||||
<span />
|
|
||||||
{isEditing
|
|
||||||
? isNotebook
|
|
||||||
? "Demo 无锁模式 · Kernel 已连接"
|
|
||||||
: "Demo 无锁模式 · 编辑中"
|
|
||||||
: latestVersion
|
|
||||||
? `最新 ${latestVersion.version_label}`
|
|
||||||
: "工作副本已就绪"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`editor-canvas${isEditing && jupyterUrl ? " is-embedded" : ""}`}>
|
|
||||||
{isEditing && jupyterUrl ? (
|
|
||||||
<section className="embedded-jupyter">
|
|
||||||
<div className="embedded-jupyter__status">
|
|
||||||
<span>
|
|
||||||
<i />
|
|
||||||
Workspace Jupyter Server
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{isNotebook ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑器"}
|
|
||||||
</span>
|
|
||||||
<code title={editSession.runtime_id}>
|
|
||||||
Runtime {editSession.runtime_id.slice(-8)}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<iframe
|
|
||||||
key={`${editSession.edit_session_id}:${jupyterUrl}`}
|
|
||||||
src={jupyterUrl}
|
|
||||||
title={`${script.script_name} Jupyter 编辑器`}
|
|
||||||
allow="clipboard-read; clipboard-write"
|
|
||||||
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-modals"
|
|
||||||
onLoad={(event) => confineJupyterFrame(event.currentTarget)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
) : isNotebook ? (
|
|
||||||
<section
|
|
||||||
className={`editor-opening-state${openError ? " has-error" : ""}`}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<div className="editor-opening-state__icon">
|
|
||||||
{openError
|
|
||||||
? <Icon name="info" size={28} />
|
|
||||||
: <span className="button-spinner button-spinner--blue" />}
|
|
||||||
</div>
|
|
||||||
<strong>
|
|
||||||
{openError ? "Notebook 打开失败" : "正在打开 Notebook"}
|
|
||||||
</strong>
|
|
||||||
<p>
|
|
||||||
{openError
|
|
||||||
? openError
|
|
||||||
: "正在获取编辑锁并连接 Workspace Jupyter Server…"}
|
|
||||||
</p>
|
|
||||||
{openError && (
|
|
||||||
<button
|
|
||||||
className="open-editor-button"
|
|
||||||
type="button"
|
|
||||||
disabled={editBusy}
|
|
||||||
onClick={onOpenEditor}
|
|
||||||
>
|
|
||||||
{editBusy
|
|
||||||
? <span className="button-spinner button-spinner--blue" />
|
|
||||||
: <Icon name="refresh" size={16} />}
|
|
||||||
{editBusy ? "正在重试…" : "重试打开"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
) : (
|
|
||||||
<section className="script-overview">
|
|
||||||
<div className="script-overview__header">
|
|
||||||
<div>
|
|
||||||
<span className="section-kicker">
|
|
||||||
PYTHON SCRIPT
|
|
||||||
</span>
|
|
||||||
<h2>{script.script_name}</h2>
|
|
||||||
<p>{script.relative_path}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={`open-editor-button${isEditing ? " is-editing" : ""}`}
|
|
||||||
type="button"
|
|
||||||
disabled={editBusy}
|
|
||||||
onClick={onOpenEditor}
|
|
||||||
>
|
|
||||||
{editBusy
|
|
||||||
? <span className="button-spinner button-spinner--blue" />
|
|
||||||
: <Icon name="external" size={17} />}
|
|
||||||
{editBusy
|
|
||||||
? "正在准备编辑器…"
|
|
||||||
: isEditing
|
|
||||||
? "继续编辑"
|
|
||||||
: "打开编辑器"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="metadata-grid">
|
|
||||||
<div>
|
|
||||||
<span>脚本类型</span>
|
|
||||||
<strong>Python</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>可见范围</span>
|
|
||||||
<strong>
|
|
||||||
{script.visibility === "workspace"
|
|
||||||
? "Workspace"
|
|
||||||
: script.visibility === "public" ? "公开" : "私有"}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>文件大小</span>
|
|
||||||
<strong>{formatBytes(script.size_bytes)}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>最近更新</span>
|
|
||||||
<strong>{formatTime(script.updated_at)}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="preview-card">
|
|
||||||
<div className="preview-card__bar">
|
|
||||||
<div>
|
|
||||||
<span className="window-dot window-dot--red" />
|
|
||||||
<span className="window-dot window-dot--yellow" />
|
|
||||||
<span className="window-dot window-dot--green" />
|
|
||||||
</div>
|
|
||||||
<span>Python 预览</span>
|
|
||||||
<em>{isEditing ? "Jupyter 中可编辑" : "平台只读预览"}</em>
|
|
||||||
</div>
|
|
||||||
<PythonPreview />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="integrity-row">
|
|
||||||
<span>
|
|
||||||
<Icon name="check" size={15} />
|
|
||||||
{isEditing ? "Demo 无锁编辑会话已连接" : "工作副本已同步"}
|
|
||||||
</span>
|
|
||||||
<span>SHA-256 {shortHash(script.content_hash)}</span>
|
|
||||||
<span>
|
|
||||||
稳定版本
|
|
||||||
{versionsLoading
|
|
||||||
? "加载中"
|
|
||||||
: latestVersion
|
|
||||||
? `${latestVersion.version_label} · ${latestVersion.versions_id}`
|
|
||||||
: "尚未发布"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PythonPreview() {
|
|
||||||
return (
|
|
||||||
<div className="python-preview">
|
|
||||||
<div className="line-numbers">1<br />2<br />3<br />4<br />5<br />6<br />7<br />8<br />9</div>
|
|
||||||
<pre>
|
|
||||||
<span className="code-comment">"""模型实验开发平台构建脚本。"""</span>
|
|
||||||
{"\n\n"}<b>def</b> <span className="code-function">main</span>() -> <b>None</b>:
|
|
||||||
{"\n"} print(<i>"Hello, Model Platform!"</i>)
|
|
||||||
{"\n\n\n"}<b>if</b> __name__ == <i>"__main__"</i>:
|
|
||||||
{"\n"} main()
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-2
@@ -1,3 +1,11 @@
|
|||||||
import { type RouteConfig, route } from "@react-router/dev/routes";
|
import { index, type RouteConfig, route } from "@react-router/dev/routes";
|
||||||
|
|
||||||
export default [route("*", "routes/platform.tsx")] satisfies RouteConfig;
|
export default [
|
||||||
|
route("login", "routes/login.tsx"),
|
||||||
|
index("routes/home.tsx"),
|
||||||
|
route("workbench", "routes/workbench.tsx"),
|
||||||
|
route("scripts", "routes/scripts.tsx"),
|
||||||
|
route("schedules", "routes/schedules.tsx"),
|
||||||
|
route("system", "routes/system.tsx"),
|
||||||
|
route("*", "routes/platform.tsx"),
|
||||||
|
] satisfies RouteConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user