diff --git a/.gitignore b/.gitignore index 07b8d35..fcd8457 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /dist-newstyle CHANGELOG.md test.json -server.py \ No newline at end of file +server.py +/workflows \ No newline at end of file diff --git a/app/Main.hs b/app/Main.hs index a58b7f9..c5152e1 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -3,7 +3,8 @@ module Main where ----------------Imports---------------- import System.Environment (getArgs) - import Data.Yaml (ParseException, decodeEither') + import System.Directory + import Data.Yaml (ParseException, decodeEither', Value (String, Null)) import Data.Aeson (encode, encodeFile) import qualified Data.ByteString.Char8 as BS @@ -12,6 +13,7 @@ module Main where import Data.Maybe (fromJust, isNothing) import Data.Either (isLeft, fromLeft, fromRight) import Control.Exception (throw) + import Text.Regex.TDFA ((=~)) --------------------------------------- @@ -24,9 +26,9 @@ module Main where main :: IO () main = getArgs >>= process >>= finish where process :: [String] -> IO Bool - process args = if length args /= 2 - then print "Please provide (1) a source and (2) a target file" >> return True - else generateJSON args >> return False + process args@[_, _] = generateJSON args >> return False + process args@["--all", src, to] = processDirectory src to >> return False + process _ = print "Please provide (1) a source and (2) a target file or provide '--all' and (1) a source and (2) a target directory" >> return True finish :: Bool -> IO () finish abort = if abort then return () else print "Done." @@ -36,6 +38,8 @@ module Main where -- exports the graph data to the JSON file specified in the second argument. generateJSON :: [String] -> IO () generateJSON args = do + print $ head args + print $ last args content <- BS.readFile (head args) let decoded = decodeEither' content :: Either ParseException Workflow if isLeft decoded then throw (fromLeft undefined decoded) else do @@ -46,6 +50,45 @@ module Main where -- encodeFile (last args) $ GData (nodeData, edgeData) encodeFile (last args) $ buildData yaml + + blackList = ["patch.yaml"] -- files not to parse when parsing the entire directory + + + -- | Processes all workflow definitions within the given directory (1) and writes the output files + -- to the other given directory (2). + processDirectory :: FilePath -> FilePath -> IO () + processDirectory src to = listDirectory src >>= filterWorkflows >>= (\ x -> generateForAll x [] Nothing) where + filterWorkflows :: [FilePath] -> IO [FilePath] + filterWorkflows entries = return $ filter (=~ ".+\\.yaml") entries + generateForAll :: [FilePath] -> [FilePath] -> Maybe FilePath -> IO () -- sources -> targets -> _index.yaml + generateForAll [] _ Nothing = fail "_index.yaml not found" + generateForAll [] targets (Just index) = writeIndex (decodeIndex index) targets "]" + generateForAll (x:xs) targets index = let (rel, abs) = defineTarget x + (newIndex, skip) = case index of + Just _ -> (index, False) + Nothing -> if x =~ ".+index\\.yaml" then (Just x, True) else (Nothing, False) + in if skip || x `elem` blackList + then generateForAll xs targets newIndex + else generateJSON [src ++ "/" ++ x, abs] >> generateForAll xs (rel:targets) newIndex + defineTarget :: FilePath -> (FilePath, FilePath) -- (rel, abs) + defineTarget x = let (path, match, _) = x =~ "[a-zA-Z0-9+._-]+\\.yaml" :: (String, String, String) + (newFile, _, _) = match =~ "\\." :: (String, String, String) + relative = "/definitions/" ++ newFile ++ ".json" + absolute = to ++ relative + in (relative, absolute) + writeIndex :: Value -> [FilePath] -> String -> IO () -- content of _index.yaml -> targets -> content for index.json + writeIndex _ [] content = writeFile (to ++ "/index.json") ('[':content) + writeIndex index (x:xs) content = let name = x + url = x + description = "" + newContent = (if null xs then "" else ",\n") ++ "{\n\"name\": \"" ++ name + ++ "\",\n\"description\": \"" + ++ description ++ "\",\n\"url\": \"" ++ url ++ "\"}" + in writeIndex index xs (newContent ++ content) + decodeIndex :: FilePath -> Value + decodeIndex _ = Null + + --------------------------------------- -- https://stackoverflow.com/questions/59903779/how-to-parse-json-with-field-of-optional-and-variant-type-in-haskell diff --git a/editor.js b/editor.js index 4a4e3cf..891a61d 100644 --- a/editor.js +++ b/editor.js @@ -64,25 +64,16 @@ function openSearchMenu(menuitem) { document.getElementById('filepanel').style.opacity = 0; function openFileDisplay() { - var panel = document.getElementById('filepanel'); - var heading = document.getElementById('fileheading'); - var curtain = document.getElementById('curtain'); - var content = document.getElementById('filecontent'); - var buttons = document.getElementById('filebuttons'); + deselect(); function callback() { - heading.innerHTML = 'Open Workflow Definition'; - for (var i = 0; i < 100; i++) { - var item = document.createElement('div'); - item.innerHTML = i; - content.appendChild(item); - } - var pStyle = window.getComputedStyle(panel); - var hStyle = window.getComputedStyle(heading); - content.style.top = heading.offsetHeight + parseFloat(pStyle.paddingTop) + parseFloat(hStyle.marginTop) + parseFloat(hStyle.marginBottom); - var bStyle = window.getComputedStyle(buttons); - content.style.bottom = buttons.offsetHeight + parseFloat(pStyle.paddingBottom) + parseFloat(bStyle.marginTop) + parseFloat(bStyle.marginBottom); + fileHeading.innerHTML = 'Open Workflow Definition'; + var pStyle = window.getComputedStyle(filePanel); + var hStyle = window.getComputedStyle(fileHeading); + fileContent.style.top = fileHeading.offsetHeight + parseFloat(pStyle.paddingTop) + parseFloat(hStyle.marginTop) + parseFloat(hStyle.marginBottom); + var bStyle = window.getComputedStyle(fileButtons); + fileContent.style.bottom = fileButtons.offsetHeight + parseFloat(pStyle.paddingBottom) + parseFloat(bStyle.marginTop) + parseFloat(bStyle.marginBottom); } - fadeIn(callback, {element: panel, max: 0.95, step: 0.025}, {element: curtain, max: 0.5, step: 0.025}); + fadeIn(callback, {element: filePanel, max: 0.95, step: 0.025}, {element: curtain, max: 0.5, step: 0.025}); closeMenuItem(); } @@ -131,11 +122,40 @@ function fadeOut(...items) { requestAnimationFrame(fade); } -// Workflow processing +// Available workflow definition files +var workflowFiles = []; +// Workflow data var workflow = {}; const wfGraph = ForceGraph(); -fetch('http://localhost:8080/test.json') +function defineOnClick(item, url) { + item.onclick = (_ => { + fetch(url) + .then(response => response.json()) + .then(data => { + closeFileDisplay(); + for (var key in data) + workflow[key] = data[key]; + prepareWorkflow(); + updateGraph(); + }); + }); +} + +fetch('http://localhost:8080/index.json') + .then(response => response.json()) + .then(data => { + workflowFiles = data; + for (var i = 0; i < workflowFiles.length; i++) { + var item = document.createElement('div'); + item.innerHTML = workflowFiles[i].name; + var url = 'http://localhost:8080' + workflowFiles[i].url; + defineOnClick(item, url); + fileContent.appendChild(item); + } + var url = 'http://localhost:8080' + data[0].url; + return fetch(url); + }) .then((response) => response.json()) .then((data) => { for (var key in data) @@ -194,10 +214,17 @@ var selection = null; // The currently selected node/edge. var rightSelection = null; // The currently right clicked node/edge. var edgeTo = null; // Target of an edge to be created. var edgeFrom = null; // Start on an edge to be created. -const sidePanel = document.getElementById('sidepanel'); +const curtain = document.getElementById('curtain'); +//Side Panel +const sidePanel = document.getElementById('sidepanel'); const sideContent = document.getElementById('sidecontent'); const sideHeading = document.getElementById('sideheading'); const sideButtons = document.getElementById('sidebuttons'); +//File panel +const filePanel = document.getElementById('filepanel'); +const fileHeading = document.getElementById('fileheading'); +const fileContent = document.getElementById('filecontent'); +const fileButtons = document.getElementById('filebuttons'); //Context menus const contextMenuBg = document.getElementById('ctmenubg'); //Click on background const contextMenuSt = document.getElementById('ctmenust'); //Click on state @@ -429,50 +456,131 @@ function computeCurvatures() { } +function prepareWorkflow() { + stateIdCounter = workflow.states ? workflow.states.length : 0; + actionIdCounter = workflow.states ? workflow.actions.length : 0; + //Parse workflow -function runnn() { - -stateIdCounter = workflow.states ? workflow.states.length : 0; -actionIdCounter = workflow.states ? workflow.actions.length : 0; + workflow.states.forEach(state => { + var messages = []; + state.stateData.messages.forEach(msg => messages.push(new Message(msg))); + state.stateData.messages = messages; + var viewers = []; + state.stateData.viewers.forEach(v => viewers.push(new Role(v))); + state.stateData.viewers = viewers; + state.stateData.payload = new Payload(state.stateData.payload); + }) -//Parse workflow + workflow.actions.forEach(action => { + var messages = []; + action.actionData.messages.forEach(msg => messages.push(new Message(msg))); + action.actionData.messages = messages; + var viewers = []; + action.actionData.viewers.forEach(v => viewers.push(new Role(v))); + action.actionData.viewers = viewers; + var actors = []; + action.actionData.actors.forEach(v => actors.push(new Role(v))); + action.actionData.actors = actors; + var viewActors = []; + action.actionData['actor Viewers'].forEach(v => viewActors.push(new Role(v))); + action.actionData['actor Viewers'] = viewActors; + action.actionData.form = new Payload(action.actionData.form); + }) -workflow.states.forEach(state => { - var messages = []; - state.stateData.messages.forEach(msg => messages.push(new Message(msg))); - state.stateData.messages = messages; - var viewers = []; - state.stateData.viewers.forEach(v => viewers.push(new Role(v))); - state.stateData.viewers = viewers; - state.stateData.payload = new Payload(state.stateData.payload); -}) + workflow.actions.forEach(act => act.actionData.actors.forEach(a => { + var includes = false; + actors.forEach(actor => includes = includes || equalRoles(a, actor)); + (!includes) && actors.push(a); + (!act.actionData.actorNames) && (act.actionData.actorNames = []); + act.actionData.actorNames.push(getRoleName(a)); + })); -workflow.actions.forEach(action => { - var messages = []; - action.actionData.messages.forEach(msg => messages.push(new Message(msg))); - action.actionData.messages = messages; - var viewers = []; - action.actionData.viewers.forEach(v => viewers.push(new Role(v))); - action.actionData.viewers = viewers; - var actors = []; - action.actionData.actors.forEach(v => actors.push(new Role(v))); - action.actionData.actors = actors; - var viewActors = []; - action.actionData['actor Viewers'].forEach(v => viewActors.push(new Role(v))); - action.actionData['actor Viewers'] = viewActors; - action.actionData.form = new Payload(action.actionData.form); -}) + //Prepare actor highlighting + var allActors = document.createElement('option'); + allActors.text = NO_ACTOR; + selectedActor.add(allActors); + actors.forEach(actor => { + var option = document.createElement('option'); + option.text = getRoleName(actor); + selectedActor.add(option); + }); -workflow.actions.forEach(act => act.actionData.actors.forEach(a => { - var includes = false; - actors.forEach(actor => includes = includes || equalRoles(a, actor)); - (!includes) && actors.push(a); - (!act.actionData.actorNames) && (act.actionData.actorNames = []); - act.actionData.actorNames.push(getRoleName(a)); -})); -// console.log(actors); -// workflow.actions.forEach(a => console.log(a.actionData.actorNames)); + //Identify all viewers of every action + workflow.actions.forEach(act => { + if (act.actionData.viewers.length === 0) { + viewableByAll.push(act.actionData); + } else { + act.actionData.viewers.forEach(v => { + var includes = false; + viewers.forEach(viewer => includes = includes || equalRoles(v, viewer)); + (!includes) && viewers.push(v); + (!act.actionData.viewerNames) && (act.actionData.viewerNames = []); + act.actionData.viewerNames.push(getRoleName(v)); + }) + } + if (act.actionData.mode === 'initial') { + act.actionData.actorNames.forEach(an => !initiators.includes(an) && initiators.push(an)); + } + }); + //Identify all viewers of every state + workflow.states.forEach(st => { + if (st.name === '@@INIT') { + initState = st; + } else if (st.stateData.viewers.length === 0) { + viewableByAll.push(st.stateData); + } else { + st.stateData.viewers.forEach(v => { + var includes = false; + viewers.forEach(viewer => includes = includes || equalRoles(v, viewer)); + (!includes) && viewers.push(v); + (!st.stateData.viewerNames) && (st.stateData.viewerNames = []); + st.stateData.viewerNames.push(getRoleName(v)); + }) + } + }); + + initState.stateData.viewerNames = initiators; + + const ALL_VIEW = "Not explicitly specified"; + if (viewableByAll.length > 0) { + viewers.push(ALL_VIEW); + var viewerNames = [] + viewers.forEach(viewer => viewerNames.push(getRoleName(viewer))); + viewableByAll.forEach(data => { + data.viewerNames = viewerNames; + }); + } + + //Prepare viewer highlighting + var allViewers = document.createElement('option'); + allViewers.text = NO_VIEWER; + selectedViewer.add(allViewers); + viewers.forEach(viewer => { + var option = document.createElement('option'); + option.text = getRoleName(viewer); + selectedViewer.add(option); + }); + + //Compute abbreviations of the names of all states + workflow.states.forEach(state => { + // var label = node.name.substring(0, 5); + var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)]; + for (var i = 0; i < label.length; i++) { + if (label[i] === '(') continue; // if the state name contains whitespace after the brace + var isBrace = label[i][0] === '('; + label[i] = label[i].substring(isBrace ? 1 : 0, isBrace ? 2 : 1); + } + labelString = label.join('').substring(0,6); + var counter = 1; + var len = labelString.length; + while (stateAbbreviations.includes(labelString)) { + labelString = labelString.substring(0,len) + "'" + counter++; + } + stateAbbreviations.push(labelString); + state.stateData.abbreviation = labelString; + }); +} function getRoleName(role) { if (typeof role == 'string') { @@ -484,80 +592,6 @@ function getRoleName(role) { } } -//Prepare actor highlighting -var allActors = document.createElement('option'); -allActors.text = NO_ACTOR; -selectedActor.add(allActors); -actors.forEach(actor => { - var option = document.createElement('option'); - option.text = getRoleName(actor); - selectedActor.add(option); -}); - -//Identify all viewers of every action -workflow.actions.forEach(act => { - if (act.actionData.viewers.length === 0) { - viewableByAll.push(act.actionData); - } else { - act.actionData.viewers.forEach(v => { - var includes = false; - viewers.forEach(viewer => includes = includes || equalRoles(v, viewer)); - (!includes) && viewers.push(v); - (!act.actionData.viewerNames) && (act.actionData.viewerNames = []); - act.actionData.viewerNames.push(getRoleName(v)); - }) - } - if (act.actionData.mode === 'initial') { - act.actionData.actorNames.forEach(an => !initiators.includes(an) && initiators.push(an)); - } -}); -//Identify all viewers of every state -workflow.states.forEach(st => { - if (st.name === '@@INIT') { - initState = st; - } else if (st.stateData.viewers.length === 0) { - viewableByAll.push(st.stateData); - } else { - st.stateData.viewers.forEach(v => { - var includes = false; - viewers.forEach(viewer => includes = includes || equalRoles(v, viewer)); - (!includes) && viewers.push(v); - (!st.stateData.viewerNames) && (st.stateData.viewerNames = []); - st.stateData.viewerNames.push(getRoleName(v)); - }) - } -}); - -initState.stateData.viewerNames = initiators; - -const ALL_VIEW = "Not explicitly specified"; -if (viewableByAll.length > 0) { - viewers.push(ALL_VIEW); - var viewerNames = [] - viewers.forEach(viewer => viewerNames.push(getRoleName(viewer))); - viewableByAll.forEach(data => { - data.viewerNames = viewerNames; - }); -} - -//Prepare viewer highlighting -var allViewers = document.createElement('option'); -allViewers.text = NO_VIEWER; -selectedViewer.add(allViewers); -viewers.forEach(viewer => { - var option = document.createElement('option'); - option.text = getRoleName(viewer); - selectedViewer.add(option); -}); - -const edgeColourDefault = '#999999ff'; -const edgeColourSelected = '#000000ff'; -const edgeColourHighlightDefault = '#6ed4d4'; -const edgeColourHighlightSelected = 'magenta'; -const edgeColourSubtleDefault = '#99999955'; -const edgeColourSubtleSelected = '#00000055'; - - /** * Checks if two roles are equal. * @param {*} role1 @@ -578,6 +612,30 @@ function equalRoles(role1, role2) { return equal; } +/** + * + * @param {*} event + * @param {HTMLElement} menu + */ +function openContextMenu(x, y, menu) { + menu.style.top = y - 25; + menu.style.left = x + 20; + fadeIn(null, {element: menu, max: 1, step: 0.1}) + // menu.style.display = 'block'; + edgeFrom = edgeTo = null; +} + + +function runnn() { + + prepareWorkflow(); + +const edgeColourDefault = '#999999ff'; +const edgeColourSelected = '#000000ff'; +const edgeColourHighlightDefault = '#6ed4d4'; +const edgeColourHighlightSelected = 'magenta'; +const edgeColourSubtleDefault = '#99999955'; +const edgeColourSubtleSelected = '#00000055'; /** * @@ -595,7 +653,7 @@ function getNodeColour(node) { } else if (node.stateData.final === 'not-ok') { return (isSelected ? '#ec4e7b' : '#e7215a') + alpha; } else { - //console.log(node.stateData.final); + return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; } } else if (node.name === '@@INIT') { return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; @@ -622,40 +680,6 @@ function getEdgeColour(edge) { return isSelected ? edgeColourSelected : edgeColourDefault; } } - -//Compute abbreviations of the names of all states -workflow.states.forEach(state => { - // var label = node.name.substring(0, 5); - var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)]; - for (var i = 0; i < label.length; i++) { - if (label[i] === '(') continue; // if the state name contains whitespace after the brace - var isBrace = label[i][0] === '('; - label[i] = label[i].substring(isBrace ? 1 : 0, isBrace ? 2 : 1); - } - labelString = label.join('').substring(0,6); - var counter = 1; - var len = labelString.length; - while (stateAbbreviations.includes(labelString)) { - labelString = labelString.substring(0,len) + "'" + counter++; - } - stateAbbreviations.push(labelString); - state.stateData.abbreviation = labelString; -}); - - -/** - * - * @param {*} event - * @param {HTMLElement} menu - */ -function openContextMenu(x, y, menu) { - menu.style.top = y - 25; - menu.style.left = x + 20; - fadeIn(null, {element: menu, max: 1, step: 0.1}) - // menu.style.display = 'block'; - edgeFrom = edgeTo = null; -} - wfGraph .linkDirectionalArrowLength(6) .linkDirectionalArrowRelPos(1) diff --git a/workflow-visualiser.cabal b/workflow-visualiser.cabal index 42615c1..18b8aa1 100644 --- a/workflow-visualiser.cabal +++ b/workflow-visualiser.cabal @@ -36,6 +36,8 @@ executable workflow-visualiser bytestring, containers, text, - vector + vector, + directory, + regex-tdfa hs-source-dirs: app default-language: Haskell2010 diff --git a/workflows/index.json b/workflows/index.json index 5b2baf2..b7b8ebe 100644 --- a/workflows/index.json +++ b/workflows/index.json @@ -1,17 +1,40 @@ -[ - { - "name": "Diploma", - "description": "", - "url": "/test.json" - }, - { - "name": "Theses", - "description": "", - "url": "/test.json" - }, - { - "name": "Recognitions", - "description": "", - "url": "/test.json" - } -] \ No newline at end of file +[{ +"name": "/definitions/theses.json", +"description": "", +"url": "/definitions/theses.json"}, +{ +"name": "/definitions/theses-media.json", +"description": "", +"url": "/definitions/theses-media.json"}, +{ +"name": "/definitions/rooms-mi.json", +"description": "", +"url": "/definitions/rooms-mi.json"}, +{ +"name": "/definitions/recognitions-ifi.json", +"description": "", +"url": "/definitions/recognitions-ifi.json"}, +{ +"name": "/definitions/master-practical-training.json", +"description": "", +"url": "/definitions/master-practical-training.json"}, +{ +"name": "/definitions/general-eo-tickets.json", +"description": "", +"url": "/definitions/general-eo-tickets.json"}, +{ +"name": "/definitions/diploma.json", +"description": "", +"url": "/definitions/diploma.json"}, +{ +"name": "/definitions/cs-minor-degrees.json", +"description": "", +"url": "/definitions/cs-minor-degrees.json"}, +{ +"name": "/definitions/cip-courses-mi.json", +"description": "", +"url": "/definitions/cip-courses-mi.json"}, +{ +"name": "/definitions/certificates.json", +"description": "", +"url": "/definitions/certificates.json"}] \ No newline at end of file