diff --git a/README.md b/README.md index 63133c2..f3f8d98 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ ## Docker + + Note: Currently the API web server infrastructure used in the docker version only supports hardcoded included OCEL2 files. Make sure to include at least some of the following OCEL2 files in `./backend/data/`: `order-management.json`, `ocel2-p2p.json`, `ContainerLogistics.json`(available at https://www.ocel-standard.org/event-logs/overview/). +### Docker Compose +Run `docker compose up --build` in the project root. + + +### Docker Files - __backend__: 1. First build using `sudo docker build ./backend -t ocedeclare-backend` @@ -9,3 +16,4 @@ Make sure to include at least some of the following OCEL2 files in `./backend/d - __frontend__: 1. First build using `sudo docker build ./frontend -t ocedeclare-frontend` 2. Then run with `sudo docker run --init -p 4567:4567 ocedeclare-backend` + diff --git a/backend/shared/src/constraints/check_constraints.rs b/backend/shared/src/constraints/check_constraints.rs index 0acc235..f1025a7 100644 --- a/backend/shared/src/constraints/check_constraints.rs +++ b/backend/shared/src/constraints/check_constraints.rs @@ -390,18 +390,21 @@ pub fn check_with_tree( BoundValue::Multiple(_) => todo!(), } } - None => linked_ocel - .objects_of_type - .get(&v.object_type) - .unwrap() - .iter() - .map(|obj| { - let mut new_bound_val = bound_val.clone(); - new_bound_val - .insert(v.name.clone(), BoundValue::Single(obj.id.clone())); - (add_info.clone(), new_bound_val) - }) - .collect_vec(), + None => match linked_ocel.objects_of_type.get(&v.object_type) { + Some(objs) => objs + .iter() + .map(|obj| { + let mut new_bound_val = bound_val.clone(); + new_bound_val + .insert(v.name.clone(), BoundValue::Single(obj.id.clone())); + (add_info.clone(), new_bound_val) + }) + .collect_vec(), + None =>{ + eprintln!("Object type {} not found!",v.object_type); + Vec::new() + }, + }, }) .collect(); } diff --git a/backend/web-server/src/main.rs b/backend/web-server/src/main.rs index 3ca29c6..98398d4 100644 --- a/backend/web-server/src/main.rs +++ b/backend/web-server/src/main.rs @@ -1,16 +1,16 @@ -use std::{ - collections::{HashMap, HashSet}, - env, - sync::{Arc, RwLock}, -}; - use axum::{ - extract::State, + body::Bytes, + extract::{DefaultBodyLimit, State}, http::{header::CONTENT_TYPE, Method, StatusCode}, routing::{get, post}, Json, Router, }; use itertools::Itertools; +use std::{ + collections::{HashMap, HashSet}, + env, + sync::{Arc, RwLock}, +}; use ocedeclare_shared::{ constraints::{check_with_tree, CheckWithTreeRequest, ViolationsWithoutID}, @@ -24,7 +24,9 @@ use ocedeclare_shared::{ preprocessing::preprocess::link_ocel_info, OCELInfo, }; -use process_mining::event_log::ocel::ocel_struct::OCEL; +use process_mining::{ + event_log::ocel::ocel_struct::OCEL, import_ocel_xml_slice, +}; use tower_http::cors::CorsLayer; use crate::load_ocel::{ @@ -55,6 +57,14 @@ async fn main() { let app = Router::new() .route("/ocel/load", post(load_ocel_file_req)) .route("/ocel/info", get(get_loaded_ocel_info)) + .route( + "/ocel/upload-json", + post(upload_ocel_json).layer(DefaultBodyLimit::disable()), + ) + .route( + "/ocel/upload-xml", + post(upload_ocel_xml).layer(DefaultBodyLimit::disable()), + ) .route("/ocel/available", get(get_available_ocels)) .route( "/ocel/event-qualifiers", @@ -88,6 +98,29 @@ pub async fn get_loaded_ocel_info( } } +async fn upload_ocel_xml( + State(state): State, + ocel_bytes: Bytes, +) -> (StatusCode, Json) { + let ocel = import_ocel_xml_slice(&ocel_bytes); + let mut x = state.ocel.write().unwrap(); + let ocel_info: OCELInfo = (&ocel).into(); + *x = Some(ocel); + + (StatusCode::OK, Json(ocel_info)) +} + +async fn upload_ocel_json( + State(state): State, + ocel_bytes: Bytes, +) -> (StatusCode, Json) { + let ocel: OCEL = serde_json::from_slice(&ocel_bytes).unwrap(); + let mut x = state.ocel.write().unwrap(); + let ocel_info: OCELInfo = (&ocel).into(); + *x = Some(ocel); + (StatusCode::OK, Json(ocel_info)) +} + pub fn with_ocel_from_state(State(state): &State, f: F) -> Option where F: FnOnce(&OCEL) -> T, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ecbf256..6bdb51a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,14 +8,22 @@ import { import { createContext, useContext, useEffect, useState } from "react"; import toast from "react-hot-toast"; // import { Outlet, useLocation } from "react-router-dom"; +import { + PiSealCheckBold +} from "react-icons/pi"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; import "./App.css"; +import { BackendProviderContext } from "./BackendProviderContext"; import MenuLink from "./components/MenuLink"; import Spinner from "./components/Spinner"; import { Button } from "./components/ui/button"; import { type OCELInfo } from "./types/ocel"; -import { BackendProviderContext } from "./BackendProviderContext"; -import { Outlet, useLocation } from "react-router-dom"; - +const VALID_OCEL_MIME_TYPES = [ + "application/json", + "text/json", + "text/xml", + "application/xml", +]; export const OcelInfoContext = createContext(undefined); function App() { @@ -23,6 +31,7 @@ function App() { const [ocelInfo, setOcelInfo] = useState(); const location = useLocation(); + const navigate = useNavigate(); const isAtRoot = location.pathname === "/"; const [availableOcels, setAvailableOcels] = useState([]); const [selectedOcel, setSelectedOcel] = useState(); @@ -35,18 +44,23 @@ function App() { error: "Failed to load OCEL info", }) .then((info) => { - setOcelInfo(info); - }); - - void toast - .promise(backend["ocel/available"](), { - loading: "Loading available OCEL", - success: "Got available OCEL", - error: "Failed to load available OCEL", - }) - .then((res) => { - setAvailableOcels(res); + if (info !== undefined) { + setOcelInfo(info); + } else { + setOcelInfo(undefined); + } }); + if (backend["ocel/available"] !== undefined) { + void toast + .promise(backend["ocel/available"](), { + loading: "Loading available OCEL", + success: "Got available OCEL", + error: "Failed to load available OCEL", + }) + .then((res) => { + setAvailableOcels(res); + }); + } }, []); async function loadOcel() { @@ -54,9 +68,13 @@ function App() { console.warn("No valid OCEL selected"); return; } + if (backend["ocel/load"] === undefined) { + console.warn("ocel/load is not supported by this backend."); + return; + } await toast.promise( backend["ocel/load"](selectedOcel).then((ocelInfo) => { - setOcelInfo(ocelInfo); + setOcelInfoAndNavigate(ocelInfo); }), { loading: "Importing OCEL...", @@ -66,15 +84,50 @@ function App() { ); } + function handleFileUpload(file: File | null) { + if (backend["ocel/upload"] === undefined) { + console.warn("No ocel/upload available!"); + return; + } + if (file != null) { + void toast + .promise(backend["ocel/upload"](file), { + loading: "Importing file...", + success: "Imported OCEL", + error: "Failed to import OCEL", + }) + .then((ocelInfo) => { + if (ocelInfo != null) { + setOcelInfoAndNavigate(ocelInfo); + } else { + setOcelInfo(undefined); + } + }); + } + } + + const showAvailableOcels = + availableOcels.length > 0 && backend["ocel/available"] !== undefined; + const filePickerAvailable = backend["ocel/picker"] !== undefined; + + function setOcelInfoAndNavigate(info: OCELInfo | undefined) { + setOcelInfo(info); + if (info !== null) { + navigate("/ocel-info"); + } + } + return (
- +
{ocelInfo !== undefined && ( - OCEL loaded + + OCEL loaded + {ocelInfo.num_events} Events {ocelInfo.num_objects} Objects @@ -82,7 +135,15 @@ function App() { {ocelInfo !== undefined && ( <> OCEL Info - Constraints + + + Constraints + )}
@@ -94,45 +155,139 @@ function App() {
- {/* */} {isAtRoot && ( -
- +

Load OCEL

+ )} + {isAtRoot && + filePickerAvailable && + backend["ocel/picker"] !== undefined && ( + )} + {isAtRoot && + showAvailableOcels && + backend["ocel/load"] !== undefined && ( +
+ + +
+ )} + + {isAtRoot && backend["ocel/upload"] !== undefined && ( +
+ {showAvailableOcels &&
OR
} +
{ + ev.preventDefault(); + const items = ev.dataTransfer.items; + if (items.length > 0 && items[0].kind === "file") { + const fileMimeType = items[0].type; + if (!VALID_OCEL_MIME_TYPES.includes(fileMimeType)) { + const fileType = + fileMimeType.length === 0 ? "" : `(${fileMimeType})`; + toast( + `Files of type ${fileType} are not supported!\n\nIf you are sure that this is an valid OCEL2 file, please select it manually by clicking on the dropzone.`, + { id: "unsupported-file" }, + ); + } + } + }} + onDrop={(ev) => { + ev.preventDefault(); + const files = ev.dataTransfer.items; + if (files.length > 0) { + const fileWrapper = files[0]; + const file = fileWrapper.getAsFile(); + if (VALID_OCEL_MIME_TYPES.includes(file?.type ?? "")) { + handleFileUpload(file); + } else { + const fileType = + file?.type == null ? "" : `(${file?.type})`; + toast( + `Files of this type ${fileType} are not supported!\n\nIf you are sure that this is an valid OCEL2 file, please select it manually by clicking on the dropzone.`, + { id: "unsupported-file" }, + ); + } + } + }} + > + +
)}
diff --git a/frontend/src/BackendProviderContext.ts b/frontend/src/BackendProviderContext.ts index adfa8cc..66d89cd 100644 --- a/frontend/src/BackendProviderContext.ts +++ b/frontend/src/BackendProviderContext.ts @@ -14,8 +14,10 @@ import { createContext } from "react"; export type BackendProvider = { "ocel/info": () => Promise; - "ocel/available": () => Promise; - "ocel/load": (name: string) => Promise; + "ocel/upload"?: (file: File) => Promise; + "ocel/available"?: () => Promise; + "ocel/load"?: (name: string) => Promise; + "ocel/picker"?: () => Promise; "ocel/check-constraints": ( variables: ObjectVariable[], nodesOrder: NewTreeNode[], @@ -36,8 +38,6 @@ export async function warnForNoBackendProvider(): Promise { export const BackendProviderContext = createContext({ "ocel/info": warnForNoBackendProvider, - "ocel/available": warnForNoBackendProvider, - "ocel/load": warnForNoBackendProvider, "ocel/check-constraints": warnForNoBackendProvider, "ocel/event-qualifiers": warnForNoBackendProvider, "ocel/object-qualifiers": warnForNoBackendProvider, @@ -55,6 +55,15 @@ export const API_WEB_SERVER_BACKEND_PROVIDER: BackendProvider = { await fetch("http://localhost:3000/ocel/available", { method: "get" }) ).json(); }, + "ocel/upload": async (ocelFile) => { + const type = ocelFile.name.endsWith(".json") ? "json" : "xml"; + return await ( + await fetch(`http://localhost:3000/ocel/upload-${type}`, { + method: "post", + body: ocelFile, + }) + ).json(); + }, "ocel/load": async (name) => { return await ( await fetch("http://localhost:3000/ocel/load", { diff --git a/frontend/src/components/MenuLink.tsx b/frontend/src/components/MenuLink.tsx index dce521f..818e080 100644 --- a/frontend/src/components/MenuLink.tsx +++ b/frontend/src/components/MenuLink.tsx @@ -1,15 +1,20 @@ import { Link } from "react-router-dom"; import { buttonVariants } from "./ui/button"; import { type ReactNode } from "react"; +import clsx, { type ClassValue } from "clsx"; type MenuLinkProps = { to: string; children: ReactNode; + classNames?: ClassValue[]; onClick?: (ev: React.MouseEvent) => any; }; export default function MenuLink(props: MenuLinkProps) { return ( diff --git a/frontend/src/routes/visual-editor/helper/node/EventTypeNode.tsx b/frontend/src/routes/visual-editor/helper/node/EventTypeNode.tsx index 187c295..059c5db 100644 --- a/frontend/src/routes/visual-editor/helper/node/EventTypeNode.tsx +++ b/frontend/src/routes/visual-editor/helper/node/EventTypeNode.tsx @@ -111,7 +111,7 @@ export default function EventTypeNode({ } const countConstraint = getCountConstraint(); const canAddObjects = - objectVariables.filter((ot) => qualifierPerObjectType[ot.type].length > 0) + objectVariables.filter((ot) => qualifierPerObjectType[ot.type]?.length > 0) .length > 0; return (
{ + if (qualifierPerObjectType[ot.type] == null) { + return []; + } return qualifierPerObjectType[ot.type] .filter( (qualifier) => diff --git a/tauri/src/main.tsx b/tauri/src/main.tsx index 193af7d..234eb09 100644 --- a/tauri/src/main.tsx +++ b/tauri/src/main.tsx @@ -20,11 +20,7 @@ const tauriBackend: BackendProvider = { const ocelInfo: OCELInfo = await invoke("get_current_ocel_info"); return ocelInfo; }, - "ocel/available": async () => { - return ["Select file manually..."]; - }, - "ocel/load": async (fileName: string) => { - console.log(fileName); + "ocel/picker": async () => { const path = await dialog.open({ title: "Select an OCEL2 file", filters: [{ name: "OCEL2", extensions: ["json", "xml"] }], @@ -33,8 +29,9 @@ const tauriBackend: BackendProvider = { const ocelInfo: OCELInfo = await invoke("import_ocel", { path }); return ocelInfo; } - throw new Error("No valid OCEL path"); + throw new Error("No file selected"); }, + "ocel/check-constraints": async (variables, nodes) => { return await invoke("check_constraint_with_tree", { variables, nodes }); },