<html>
    <head>
        <meta charset="UTF-8">
        <title>Test scheduler</title>
        <style>
body {
    font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}

table {
    border: 0px;
    border-spacing: 0px 3px;
}

td {
    padding: 1em;
    border: 0px;
    margin: 0px;
}

td.schedule-path, th.schedule-path {
    width: 20em;
}

.iconButton {
    height: 2.2em;
    width: 2.2em;
}

.menuEntry {
    display: block;
    color: whitesmoke;
    text-decoration: none;
    margin: .3em;
    padding: 1em;
}

.menuEntry:hover {
    background-color: darkslategray;
}

.menuView {
    position: absolute;
    display: block;
    background-color: black;
    padding: .5em;
    border-radius: .5em;
}

.logView {
    overflow-y: auto;
    border: 2px solid #ddd;
    top: 30px;
    bottom: 0px;
    right: 0px;
    left: 0px;
    padding: .4em;
    font-family: monospace;
    position: absolute;
}

.logViewWindow {
    position: fixed;
    float: left;
    left: 10%;
    right: 10%;
    top: 10%;
    bottom: 10%;
    background-color: white;
    width: 75%;
    height: 75%;
}

.logViewTab {
    height: 30px;
}
        </style>
    </head>
    <body>
        <script>
'use strict';

(() => {

const scheduleTable = document.createElement('table');
const schedule = {};
let logView = null;
let menuView = null;

function init() {
    scheduleTable.style.display = 'none';
    document.body.appendChild(scheduleTable);
    scheduleTable.appendChild(makeScheduleRow(['ID', 'Created At', 'Path', 'Template', 'State', 'Actions'], 'th'));

    document.addEventListener("keydown", function(event) {
        if (event.which == 27) {
            closeLogView();
            closeJobMenu();
        }
    });
    const bodyClickListener = event => {
        const modals = [
            {element: menuView, callback: closeJobMenu},
            {element: logView, callback: closeLogView}
        ];
        for (const modal of modals) {
            if (modal.element === null || !document.body.contains(modal.element)) {
                continue;
            }
            if (!modal.element.contains(event.target)) {
                console.log("Clicked outside of " + modal.element.className);
                modal.callback();
            }
        }
    }

    document.addEventListener('click', bodyClickListener);
}


function onStatusGet(status) {
    scheduleTable.style.display = 'block';


    const incomingIds = status.schedule.map(s => s.id);

    const newTasksIds = incomingIds.filter(i => !(i in schedule));
    const delTasksIds = Object.keys(schedule).filter(i => incomingIds.indexOf(i) == -1);

    // Delete tasks that are not on the server anymore
    for (const i of delTasksIds) {
        const row = schedule[i].row;
        row.parentElement.removeChild(row);
        delete schedule[i];
    }

    // Add rows for new incoming tasks
    for (const i of newTasksIds) {
        const row = makeScheduleRow([i, '', '', '', '', '']);
        const s = { id: i, row };
        schedule[s.id] = s;

        // Find out correct placement in table
        let ids = Object.keys(schedule);
        ids.sort(i => i | 0);
        
        const idx = ids.indexOf(s.id);
        if (idx == -1) {
            throw new Error(s.id + " NOT FOUND");
        } else if (idx == 0) {
            scheduleTable.appendChild(row);
        } else {
            const prevId = ids[idx-1];
            scheduleTable.insertBefore(row, schedule[prevId].row);
        }

        // Add buttons for actions
        const lastCell = row.cells[row.cells.length - 1];
        function makeButton(icon, onClick) {
            const button = document.createElement('button');
            button.innerHTML = icon;
            button.onclick = onClick;
            button.className = 'iconButton'
            return button;
        }
        lastCell.appendChild(makeButton('...', (e) => showJobMenu(e, s.id)));
    }

    // Update already existing tasks
    for (const s of status.schedule) {
        for (const key in s) {
            schedule[s.id][key] = s[key];
        }

        const row = schedule[s.id].row;
        updateInnerText(row.cells[1], s.added.substr(0, 16)
            + '\n' + s.added.substr(17));
        updateInnerText(row.cells[2], s.path);
        updateInnerText(row.cells[3], s.template);
        updateInnerText(row.cells[4], s.state);
        if (s.state == 'FAILED') {
            row.style.backgroundColor = '#fcc';
        } else if (s.state == 'SUCCESSFUL') {
            row.style.backgroundColor = '#cfc';
        } else if (s.state == 'RUNNING') {
            row.style.backgroundColor = '#ff9';
        } else {
            row.style.backgroundColor = '#eee';
        }
    }

}


function updateInnerText(element, text) {
    if (element.innerText !== text) {
        element.innerText = text;
    }
}


function showJobMenu(event, jobId) {
    closeJobMenu();

    menuView = document.createElement('div');
    menuView.className = 'menuView';
    menuView.style.left = 0;
    menuView.style.top = 0;
    menuView.style.width = 'auto';
    menuView.style.height = 'auto';
    function makeEntry(label, onClick) {
        const button = document.createElement('a');
        button.innerHTML = label;
        button.onclick = (e) => {closeJobMenu(); onClick(e);};
        button.href = 'javascript:;';
        button.className = 'menuEntry';
        return button;
    }
    const st = schedule[jobId].state;
    if (st == 'SUCCESSFUL' || st == 'FAILED') {
        menuView.appendChild(makeEntry('↺ Retry', () => restartJob(jobId)));
    }
    if (st == 'SUCCESSFUL' || st == 'FAILED' || st == 'SCHEDULED') {
        menuView.appendChild(makeEntry('🗑️ Delete', () => deleteJob(jobId)));
    }
    if (st == 'RUNNING') {
        menuView.appendChild(makeEntry('🗴 Cancel', () => cancelJob(jobId)));
    }
    menuView.appendChild(makeEntry('🗎 View Logs', () => viewJobLogs(jobId)));
    if (st == 'SUCCESSFUL') {
        menuView.appendChild(makeEntry('↓ Download results zip', () => getResultZip(jobId)));
        menuView.appendChild(makeEntry('↓ Download results JSON', () => getResultJSON(jobId)));
    }

    function onLayout() {
        // This gets executed after the browser did the layout for the menu,
        // we have the size of the menu view, now we have to place it close to
        // the bounding box of the button that was clicked, without putting it
        // ouside the screen:
        const a = document.body.getBoundingClientRect();
        const b = menuView.getBoundingClientRect();
        const r = event.target.getBoundingClientRect();
        const vh = window.innerHeight || document.documentElement.clientHeight;
        const vw = window.innerWidth || document.documentElement.clientWidth;


        menuView.style.width = b.width + 'px';
        menuView.style.height = b.height + 'px';
        menuView.style.top = (-a.top + Math.min(r.y, vh - b.height - 20)) + 'px';
        menuView.style.left = (-a.left + Math.min(r.x, vw - b.width - 20)) + 'px';
    };

    setTimeout(() => {
        // Execution needs to be delayed to prevent the ouside click event
        // from closing this window
        document.body.appendChild(menuView);
        setTimeout(onLayout, 1);
    }, 0);

}


function closeJobMenu() {
    if (menuView === null) {
        return;
    }
    while (menuView.childNodes.length > 0)
        menuView.removeChild(menuView.lastChild);
    if (menuView.parentElement)
        menuView.parentElement.removeChild(menuView);
    menuView = null;
}


function restartJob(jobId) {
    const xhttp = new XMLHttpRequest();
    xhttp.open("GET", "/restart_test/" + jobId, true);
    xhttp.send();
}


function deleteJob(jobId) {
    if (confirm("Confirm deleting job " + jobId + "?")) {
        const xhttp = new XMLHttpRequest();
        xhttp.open("GET", "/delete_test/" + jobId, true);
        xhttp.send();
    }
}


function cancelJob(jobId) {
    const xhttp = new XMLHttpRequest();
    xhttp.open("GET", "/cancel_test/" + jobId, true);
    xhttp.send();
}


function getResultZip(jobId) {
    window.open('/results/' + jobId + '/results.zip');
}


function getResultJSON(jobId) {
    window.open('/results/' + jobId + '/results.json');
}


function viewJobLogs(jobId) {
    const logIds = [0,1,2,3];
    const logNames = ['make stdout', 'make stderr', 'test stdout', 'test stderr'];

    logView = document.createElement('div');
    logView.className = 'logViewWindow';
    const closeLogViewButton = document.createElement('button');
    logView.appendChild(closeLogViewButton);
    closeLogViewButton.innerText = "X";
    closeLogViewButton.style.display = 'block';
    closeLogViewButton.style.right = '0px';
    closeLogViewButton.style.top = '0px';
    closeLogViewButton.style.position = 'absolute';
    closeLogViewButton.onclick = closeLogView;

    const tabs = logIds.map(logId => {
        const v = document.createElement('button');
        v.innerText = logNames[logIds.indexOf(logId)];
        v.className = 'logViewTab';
        logView.appendChild(v);
        return v;
    });

    logView.appendChild(document.createElement('br'));

    const views = logIds.map(logId => {
        const v = document.createElement('div');
        v.style.display = logId == logIds[0] ? 'block' : 'none';
        v.className = 'logView';
        logView.appendChild(v);
        const xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                let log = this.responseText;
                v.innerText = log;
                log = v.innerHTML;
                log = log.replace(/\n/g, '<br/>');
                v.innerHTML = log;
            }
        };
        xhttp.open("GET", "/view_log/" + jobId + "/" + logId, true);
        xhttp.send();
        return v;
    });

    for (const i in logIds) {
        const tab = tabs[i];
        const view = views[i];
        tab.onclick = () => {
            views.forEach(v => {v.style.display = 'none';});
            view.style.display = 'block';
        };
    }

    setTimeout(() => {
        // Execution needs to be delayed to prevent the ouside click event
        // from closing this window
        document.body.appendChild(logView);
    }, 0);
}


function closeLogView() {
    if (logView === null) {
        return;
    }
    while (logView.childElementCount > 1) {
        logView.removeChild(logView.lastElementChild);
    }
    if (logView.parentElement) {
        logView.parentElement.removeChild(logView);
    }
    logView = null;
}


function requestStatus() {
    const xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
        if (this.readyState == 4) {
            setTimeout(requestStatus, 1000);
            if (this.status == 200) {
                onStatusGet(JSON.parse(this.responseText));
            }
        }
    };
    xhttp.open("GET", "/status", true);
    xhttp.send();
}


function makeScheduleRow(list, cellTagName='td') {
    const headerRow = document.createElement('tr');
    const classes = ['schedule-id', 'schedule-date', 'schedule-path',
        'schedule-template', 'schedule-state', 'schedule-actions'];
    (list.map((text, idx) => {
        const cell = document.createElement(cellTagName);
        cell.className = classes[idx];
        cell.appendChild(document.createTextNode(text));
        return cell;
    })).forEach((n) => headerRow.appendChild(n));
    return headerRow;
};


init();

requestStatus();
})();
        </script>
    </body>
</html>