all wflows are parsed & then requested on demand

This commit is contained in:
David Mosbach 2023-05-30 02:02:13 +02:00
parent 874d81b3af
commit 61d9f9f739
5 changed files with 283 additions and 190 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
/dist-newstyle /dist-newstyle
CHANGELOG.md CHANGELOG.md
test.json test.json
server.py server.py
/workflows

View File

@ -3,7 +3,8 @@ module Main where
----------------Imports---------------- ----------------Imports----------------
import System.Environment (getArgs) 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 Data.Aeson (encode, encodeFile)
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
@ -12,6 +13,7 @@ module Main where
import Data.Maybe (fromJust, isNothing) import Data.Maybe (fromJust, isNothing)
import Data.Either (isLeft, fromLeft, fromRight) import Data.Either (isLeft, fromLeft, fromRight)
import Control.Exception (throw) import Control.Exception (throw)
import Text.Regex.TDFA ((=~))
--------------------------------------- ---------------------------------------
@ -24,9 +26,9 @@ module Main where
main :: IO () main :: IO ()
main = getArgs >>= process >>= finish where main = getArgs >>= process >>= finish where
process :: [String] -> IO Bool process :: [String] -> IO Bool
process args = if length args /= 2 process args@[_, _] = generateJSON args >> return False
then print "Please provide (1) a source and (2) a target file" >> return True process args@["--all", src, to] = processDirectory src to >> return False
else generateJSON args >> 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 :: Bool -> IO ()
finish abort = if abort then return () else print "Done." 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. -- exports the graph data to the JSON file specified in the second argument.
generateJSON :: [String] -> IO () generateJSON :: [String] -> IO ()
generateJSON args = do generateJSON args = do
print $ head args
print $ last args
content <- BS.readFile (head args) content <- BS.readFile (head args)
let decoded = decodeEither' content :: Either ParseException Workflow let decoded = decodeEither' content :: Either ParseException Workflow
if isLeft decoded then throw (fromLeft undefined decoded) else do 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) $ GData (nodeData, edgeData)
encodeFile (last args) $ buildData yaml 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 -- https://stackoverflow.com/questions/59903779/how-to-parse-json-with-field-of-optional-and-variant-type-in-haskell

358
editor.js
View File

@ -64,25 +64,16 @@ function openSearchMenu(menuitem) {
document.getElementById('filepanel').style.opacity = 0; document.getElementById('filepanel').style.opacity = 0;
function openFileDisplay() { function openFileDisplay() {
var panel = document.getElementById('filepanel'); deselect();
var heading = document.getElementById('fileheading');
var curtain = document.getElementById('curtain');
var content = document.getElementById('filecontent');
var buttons = document.getElementById('filebuttons');
function callback() { function callback() {
heading.innerHTML = 'Open Workflow Definition'; fileHeading.innerHTML = 'Open Workflow Definition';
for (var i = 0; i < 100; i++) { var pStyle = window.getComputedStyle(filePanel);
var item = document.createElement('div'); var hStyle = window.getComputedStyle(fileHeading);
item.innerHTML = i; fileContent.style.top = fileHeading.offsetHeight + parseFloat(pStyle.paddingTop) + parseFloat(hStyle.marginTop) + parseFloat(hStyle.marginBottom);
content.appendChild(item); var bStyle = window.getComputedStyle(fileButtons);
} fileContent.style.bottom = fileButtons.offsetHeight + parseFloat(pStyle.paddingBottom) + parseFloat(bStyle.marginTop) + parseFloat(bStyle.marginBottom);
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);
} }
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(); closeMenuItem();
} }
@ -131,11 +122,40 @@ function fadeOut(...items) {
requestAnimationFrame(fade); requestAnimationFrame(fade);
} }
// Workflow processing // Available workflow definition files
var workflowFiles = [];
// Workflow data
var workflow = {}; var workflow = {};
const wfGraph = ForceGraph(); 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((response) => response.json())
.then((data) => { .then((data) => {
for (var key in 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 rightSelection = null; // The currently right clicked node/edge.
var edgeTo = null; // Target of an edge to be created. var edgeTo = null; // Target of an edge to be created.
var edgeFrom = null; // Start on 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 sideContent = document.getElementById('sidecontent');
const sideHeading = document.getElementById('sideheading'); const sideHeading = document.getElementById('sideheading');
const sideButtons = document.getElementById('sidebuttons'); 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 //Context menus
const contextMenuBg = document.getElementById('ctmenubg'); //Click on background const contextMenuBg = document.getElementById('ctmenubg'); //Click on background
const contextMenuSt = document.getElementById('ctmenust'); //Click on state 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() { workflow.states.forEach(state => {
var messages = [];
stateIdCounter = workflow.states ? workflow.states.length : 0; state.stateData.messages.forEach(msg => messages.push(new Message(msg)));
actionIdCounter = workflow.states ? workflow.actions.length : 0; 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 => { workflow.actions.forEach(act => act.actionData.actors.forEach(a => {
var messages = []; var includes = false;
state.stateData.messages.forEach(msg => messages.push(new Message(msg))); actors.forEach(actor => includes = includes || equalRoles(a, actor));
state.stateData.messages = messages; (!includes) && actors.push(a);
var viewers = []; (!act.actionData.actorNames) && (act.actionData.actorNames = []);
state.stateData.viewers.forEach(v => viewers.push(new Role(v))); act.actionData.actorNames.push(getRoleName(a));
state.stateData.viewers = viewers; }));
state.stateData.payload = new Payload(state.stateData.payload);
})
workflow.actions.forEach(action => { //Prepare actor highlighting
var messages = []; var allActors = document.createElement('option');
action.actionData.messages.forEach(msg => messages.push(new Message(msg))); allActors.text = NO_ACTOR;
action.actionData.messages = messages; selectedActor.add(allActors);
var viewers = []; actors.forEach(actor => {
action.actionData.viewers.forEach(v => viewers.push(new Role(v))); var option = document.createElement('option');
action.actionData.viewers = viewers; option.text = getRoleName(actor);
var actors = []; selectedActor.add(option);
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.actions.forEach(act => act.actionData.actors.forEach(a => { //Identify all viewers of every action
var includes = false; workflow.actions.forEach(act => {
actors.forEach(actor => includes = includes || equalRoles(a, actor)); if (act.actionData.viewers.length === 0) {
(!includes) && actors.push(a); viewableByAll.push(act.actionData);
(!act.actionData.actorNames) && (act.actionData.actorNames = []); } else {
act.actionData.actorNames.push(getRoleName(a)); act.actionData.viewers.forEach(v => {
})); var includes = false;
// console.log(actors); viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
// workflow.actions.forEach(a => console.log(a.actionData.actorNames)); (!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) { function getRoleName(role) {
if (typeof role == 'string') { 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. * Checks if two roles are equal.
* @param {*} role1 * @param {*} role1
@ -578,6 +612,30 @@ function equalRoles(role1, role2) {
return equal; 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') { } else if (node.stateData.final === 'not-ok') {
return (isSelected ? '#ec4e7b' : '#e7215a') + alpha; return (isSelected ? '#ec4e7b' : '#e7215a') + alpha;
} else { } else {
//console.log(node.stateData.final); return (isSelected ? '#ffbc15' : '#eeaa00') + alpha;
} }
} else if (node.name === '@@INIT') { } else if (node.name === '@@INIT') {
return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; return (isSelected ? '#ffbc15' : '#eeaa00') + alpha;
@ -622,40 +680,6 @@ function getEdgeColour(edge) {
return isSelected ? edgeColourSelected : edgeColourDefault; 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 wfGraph
.linkDirectionalArrowLength(6) .linkDirectionalArrowLength(6)
.linkDirectionalArrowRelPos(1) .linkDirectionalArrowRelPos(1)

View File

@ -36,6 +36,8 @@ executable workflow-visualiser
bytestring, bytestring,
containers, containers,
text, text,
vector vector,
directory,
regex-tdfa
hs-source-dirs: app hs-source-dirs: app
default-language: Haskell2010 default-language: Haskell2010

View File

@ -1,17 +1,40 @@
[ [{
{ "name": "/definitions/theses.json",
"name": "Diploma", "description": "",
"description": "", "url": "/definitions/theses.json"},
"url": "/test.json" {
}, "name": "/definitions/theses-media.json",
{ "description": "",
"name": "Theses", "url": "/definitions/theses-media.json"},
"description": "", {
"url": "/test.json" "name": "/definitions/rooms-mi.json",
}, "description": "",
{ "url": "/definitions/rooms-mi.json"},
"name": "Recognitions", {
"description": "", "name": "/definitions/recognitions-ifi.json",
"url": "/test.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"}]