mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 04:18:42 -06:00
Added Workflow Listing Functionality
This commit is contained in:
@@ -1,6 +1,4 @@
|
|||||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Workflow_List.jsx
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
|
|
||||||
import React, { useState, useMemo } from "react";
|
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
Box,
|
Box,
|
||||||
@@ -15,11 +13,53 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { PlayCircle as PlayCircleIcon } from "@mui/icons-material";
|
import { PlayCircle as PlayCircleIcon } from "@mui/icons-material";
|
||||||
|
|
||||||
|
function formatDateTime(dateString) {
|
||||||
|
if (!dateString) return "";
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date)) return "";
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const year = date.getFullYear();
|
||||||
|
let hours = date.getHours();
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
const ampm = hours >= 12 ? "PM" : "AM";
|
||||||
|
hours = hours % 12 || 12;
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}${ampm}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WorkflowList({ onOpenWorkflow }) {
|
export default function WorkflowList({ onOpenWorkflow }) {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [orderBy, setOrderBy] = useState("name");
|
const [orderBy, setOrderBy] = useState("name");
|
||||||
const [order, setOrder] = useState("asc");
|
const [order, setOrder] = useState("asc");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/storage/load_workflows");
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!alive) return;
|
||||||
|
|
||||||
|
const mapped = (data.workflows || []).map(w => ({
|
||||||
|
...w,
|
||||||
|
name: w.tab_name && w.tab_name.trim() ? w.tab_name.trim() : w.file_name,
|
||||||
|
description: "",
|
||||||
|
category: "",
|
||||||
|
lastEdited: w.last_edited,
|
||||||
|
lastEditedEpoch: w.last_edited_epoch
|
||||||
|
}));
|
||||||
|
setRows(mapped);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load workflows:", err);
|
||||||
|
setRows([]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSort = (col) => {
|
const handleSort = (col) => {
|
||||||
if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc");
|
if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc");
|
||||||
else {
|
else {
|
||||||
@@ -31,6 +71,11 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
const dir = order === "asc" ? 1 : -1;
|
const dir = order === "asc" ? 1 : -1;
|
||||||
return [...rows].sort((a, b) => {
|
return [...rows].sort((a, b) => {
|
||||||
|
if (orderBy === "lastEdited" || orderBy === "lastEditedEpoch") {
|
||||||
|
const A = Number(a.lastEditedEpoch || 0);
|
||||||
|
const B = Number(b.lastEditedEpoch || 0);
|
||||||
|
return (A - B) * dir;
|
||||||
|
}
|
||||||
const A = a[orderBy] || "";
|
const A = a[orderBy] || "";
|
||||||
const B = b[orderBy] || "";
|
const B = b[orderBy] || "";
|
||||||
return String(A).localeCompare(String(B)) * dir;
|
return String(A).localeCompare(String(B)) * dir;
|
||||||
@@ -39,7 +84,7 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
|
|
||||||
const handleNewWorkflow = () => {
|
const handleNewWorkflow = () => {
|
||||||
if (onOpenWorkflow) {
|
if (onOpenWorkflow) {
|
||||||
onOpenWorkflow(); // trigger App.jsx to open editor
|
onOpenWorkflow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +94,26 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderNameCell = (r) => {
|
||||||
|
const hasPrefix = r.breadcrumb_prefix && r.breadcrumb_prefix.length > 0;
|
||||||
|
const primary = r.tab_name && r.tab_name.trim().length > 0 ? r.tab_name.trim() : r.file_name;
|
||||||
|
return (
|
||||||
|
<Box component="span">
|
||||||
|
{hasPrefix && (
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{ color: "#6b6b6b", mr: 0.5 }}
|
||||||
|
>
|
||||||
|
{r.breadcrumb_prefix} {">"}{" "}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography component="span" sx={{ color: "#e6edf3" }}>
|
||||||
|
{primary}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}>
|
<Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}>
|
||||||
<Box sx={{ p: 2, pb: 1, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<Box sx={{ p: 2, pb: 1, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
@@ -124,10 +189,10 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
onClick={() => handleRowClick(r)}
|
onClick={() => handleRowClick(r)}
|
||||||
>
|
>
|
||||||
<TableCell>{r.name}</TableCell>
|
<TableCell>{renderNameCell(r)}</TableCell>
|
||||||
<TableCell>{r.description}</TableCell>
|
<TableCell>{r.description}</TableCell>
|
||||||
<TableCell>{r.category}</TableCell>
|
<TableCell>{r.category}</TableCell>
|
||||||
<TableCell>{r.lastEdited}</TableCell>
|
<TableCell>{formatDateTime(r.lastEdited)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{sorted.length === 0 && (
|
{sorted.length === 0 && (
|
||||||
|
@@ -10,7 +10,9 @@ from flask_socketio import SocketIO, emit
|
|||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import os # To Read Production ReactJS Server Folder
|
import os # To Read Production ReactJS Server Folder
|
||||||
|
import json # For reading workflow JSON files
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
# Borealis Python API Endpoints
|
# Borealis Python API Endpoints
|
||||||
from Python_API_Endpoints.ocr_engines import run_ocr_on_base64
|
from Python_API_Endpoints.ocr_engines import run_ocr_on_base64
|
||||||
@@ -83,6 +85,113 @@ def ocr_endpoint():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# Borealis Storage API Endpoints
|
||||||
|
# ---------------------------------------------
|
||||||
|
def _safe_read_json(path: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Try to read JSON safely. Returns {} on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _extract_tab_name(obj: Dict) -> str:
|
||||||
|
"""
|
||||||
|
Best-effort extraction of a workflow tab name from a JSON object.
|
||||||
|
Falls back to empty string when unknown.
|
||||||
|
"""
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return ""
|
||||||
|
for key in ["tabName", "tab_name", "name", "title"]:
|
||||||
|
val = obj.get(key)
|
||||||
|
if isinstance(val, str) and val.strip():
|
||||||
|
return val.strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@app.route("/api/storage/load_workflows", methods=["GET"])
|
||||||
|
def load_workflows():
|
||||||
|
"""
|
||||||
|
Scan <ProjectRoot>/Workflows for *.json files and return a table-friendly list.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"root": "<absolute path to Workflows>",
|
||||||
|
"workflows": [
|
||||||
|
{
|
||||||
|
"name": "FolderA > Sub > File.json", # breadcrumb styled name for table display
|
||||||
|
"breadcrumb_prefix": "FolderA > Sub", # prefix only, to allow UI styling
|
||||||
|
"file_name": "File.json", # base filename
|
||||||
|
"rel_path": "FolderA/Sub/File.json", # path relative to Workflows
|
||||||
|
"tab_name": "Optional Tab Name", # best-effort read from JSON (may be "")
|
||||||
|
"description": "", # placeholder for future use
|
||||||
|
"category": "", # placeholder for future use
|
||||||
|
"last_edited": "YYYY-MM-DDTHH:MM:SS", # local time ISO-like string
|
||||||
|
"last_edited_epoch": 1712345678.123 # numeric, for client-side sorting
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Resolve <ProjectRoot>/Workflows relative to this file at <ProjectRoot>/Data/server.py
|
||||||
|
workflows_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "Workflows"))
|
||||||
|
results: List[Dict] = []
|
||||||
|
|
||||||
|
if not os.path.isdir(workflows_root):
|
||||||
|
# Directory missing is not a hard error; return empty set and the resolved path for visibility.
|
||||||
|
return jsonify({
|
||||||
|
"root": workflows_root,
|
||||||
|
"workflows": [],
|
||||||
|
"warning": "Workflows directory not found."
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(workflows_root):
|
||||||
|
for fname in files:
|
||||||
|
if not fname.lower().endswith(".json"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
full_path = os.path.join(root, fname)
|
||||||
|
rel_path = os.path.relpath(full_path, workflows_root) # e.g. SuperStuff/Example.json
|
||||||
|
|
||||||
|
# Build breadcrumb-style display name: "SuperStuff > Example.json"
|
||||||
|
parts = rel_path.split(os.sep)
|
||||||
|
folder_parts = parts[:-1]
|
||||||
|
breadcrumb_prefix = " > ".join(folder_parts) if folder_parts else ""
|
||||||
|
display_name = f"{breadcrumb_prefix} > {fname}" if breadcrumb_prefix else fname
|
||||||
|
|
||||||
|
# Best-effort read of tab name (not required for now)
|
||||||
|
obj = _safe_read_json(full_path)
|
||||||
|
tab_name = _extract_tab_name(obj)
|
||||||
|
|
||||||
|
# File timestamps
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(full_path)
|
||||||
|
except Exception:
|
||||||
|
mtime = 0.0
|
||||||
|
last_edited_str = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(mtime))
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"name": display_name,
|
||||||
|
"breadcrumb_prefix": breadcrumb_prefix,
|
||||||
|
"file_name": fname,
|
||||||
|
"rel_path": rel_path.replace(os.sep, "/"),
|
||||||
|
"tab_name": tab_name,
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"last_edited": last_edited_str,
|
||||||
|
"last_edited_epoch": mtime
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort newest-first by modification time
|
||||||
|
results.sort(key=lambda x: x.get("last_edited_epoch", 0.0), reverse=True)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"root": workflows_root,
|
||||||
|
"workflows": results
|
||||||
|
})
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
# Borealis Agent API Endpoints
|
# Borealis Agent API Endpoints
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
Reference in New Issue
Block a user