diff --git a/bin/docker-plugin.js b/bin/docker-plugin.js index 7b6b207..18e2e2d 100644 --- a/bin/docker-plugin.js +++ b/bin/docker-plugin.js @@ -62,6 +62,7 @@ const docker = new Docker(dockerOpts) const ENTRYPOINT_PATH = process.env['ENTRYPOINT_PATH'] || '/cronicle.sh' const cname = 'cronicle-' + (process.env['JOB_ID'] || process.pid) let imageName = process.env['IMAGE'] || 'alpine' +let network = process.env['NETWORK'] let script = process.env['SCRIPT'] ?? "#!/bin/sh\necho 'No script specified'" const autoPull = !!parseInt(process.env['PULL_IMAGE']) const autoRemove = !parseInt(process.env['KEEP_CONTAINER']) @@ -140,6 +141,8 @@ if (!keepEntrypoint) { createOptions.WorkingDir = path.dirname(ENTRYPOINT_PATH) } +if(network) createOptions.HostConfig['NetworkMode'] = network + // ----------------RUNNING CONTAINER -------- // diff --git a/bin/manager b/bin/manager index 57c8cd4..8b3c771 100755 --- a/bin/manager +++ b/bin/manager @@ -3,6 +3,8 @@ HOMEDIR="$(dirname "$(cd -- "$(dirname "$(readlink -f "$0")")" && (pwd -P 2>/dev usage() { echo "USAGE: ./manager [ --port 3012 ] [ --storage /path/to/storage.json ] [ --key secreKey ] [ --color ] " + echo " [ --reset ] # set current host as manager" + echo " [ --cluster "server1,server2" ] # add extra workers on setup" exit 1 } @@ -15,6 +17,8 @@ while (( "$#" )); do --key ) shift; key=$1 ;; --storage ) shift; storage=$1 ;; --sqlite ) shift; sqlite=$1 ;; + --reset ) shift; reset=1 ;; + --cluster ) shift; cluster=$1 ;; --help ) usage ;; -*) echo "invalid parameter: $1"; usage ;; esac @@ -44,12 +48,29 @@ if [[ $key ]]; then echo "Custom secret key set: *****" fi +if [[ $cluster ]]; then + export CRONICLE_cluster="$cluster" + echo "Following nodes will be added on setup: $cluster" +fi + # pull data from git if needed # if [ ! -d data/global ] && [ -v GIT_REPO ]; then # git clone $GIT_REPO $HOMEDIR/data # fi -$HOMEDIR/bin/control.sh setup +# check for custom nodejs binary +if [ -f $HOMEDIR/nodejs/bin/node ]; then + export PATH="$HOMEDIR/nodejs/bin:$PATH" + echo "using custom node version: $(node -v)" +fi + + +# setup storage OR make current host the primary manager if needed +if [ "$reset" = 1 ]; then + node $HOMEDIR/bin/storage-cli.js reset +else $HOMEDIR/bin/control.sh setup +fi + if [ -f "$HOMEDIR/logs/cronicled.pid" ]; then echo 'removing old pid file' @@ -63,12 +84,6 @@ if [ -f "$HOMEDIR/data/backup.json" ]; then rm "$HOMEDIR/data/backup.json" fi -# check for custom nodejs binary -if [ -f $HOMEDIR/nodejs/bin/node ]; then - export PATH="$HOMEDIR/nodejs/bin:$PATH" - echo "using custom node version: $(node -v)" -fi - BINARY="node $HOMEDIR/lib/main.js" # check if bundle exist if [ -f "$HOMEDIR/bin/cronicle.js" ]; then diff --git a/bin/manager.bat b/bin/manager.bat index 6e209a1..713a006 100644 --- a/bin/manager.bat +++ b/bin/manager.bat @@ -43,8 +43,22 @@ if /I "%1"=="--port" ( echo Using sqlite as storage: %~f2 shift shift +) else if /I "%1"=="--cluster" ( + if "%2"=="" ( + echo Missing cluster value. Specify comma-separatd hostnames + exit + ) + set CRONICLE_cluster=%2 + echo These servers will be added on setup: %2 + shift + shift +) else if /I "%1"=="--reset" ( + set CRONICLE_RESET=1 + shift ) else if /I "%1"=="--help" ( - echo Usage: .\manager [--port port] [ --storage /path/to/storage.json] + echo Usage: .\manager [--port port] [ --storage /path/to/storage.json] + echo [ --reset ] # make current host the manager + echo [ --cluster "server1,server2"] # add extra workers on setup shift ) else (exit) @@ -59,11 +73,12 @@ IF EXIST "%~dp0..\nodejs\node.exe" ( SET "PATH=%~dp0..\nodejs;%PATH%" ) -node .\storage-cli.js setup - -if not "%~1"=="" ( - set "CRONICLE_WebServer__http_port=%1" - echo CRONICLE_http_port is set to %1 +REM setup or reset manager +if "%CRONICLE_RESET%"=="1" ( + node .\storage-cli.js reset + echo Croncile manager was reset to current host +) else ( + node .\storage-cli.js setup ) node .\cronicle.js --manager --echo --foreground --color diff --git a/bin/sshx-plugin.js b/bin/sshx-plugin.js index d991f0d..980fd6d 100644 --- a/bin/sshx-plugin.js +++ b/bin/sshx-plugin.js @@ -9,7 +9,10 @@ const fs = require('fs') // read job info from stdin (sent by Cronicle engine) const job = JSON.parse(fs.readFileSync(process.stdin.fd)) -const print = (text) => process.stdout.write(text + EOL) +let pref = '' +if(job.params.annotate) pref = `[${new Date().toISOString()}] ` + +const print = (text) => process.stdout.write(pref + text + EOL) const printInfo = (text) => process.stdout.write(`[INFO] \x1b[32m${text}\x1b[0m` + EOL) const printWarning = (text) => process.stdout.write(`[INFO] \x1b[33m${text}\x1b[0m` + EOL) const printError = (text) => process.stdout.write(`\x1b[31m${text}\x1b[0m` + EOL) diff --git a/bin/storage-cli.js b/bin/storage-cli.js index 6e7ed85..7e44a99 100755 --- a/bin/storage-cli.js +++ b/bin/storage-cli.js @@ -10,6 +10,7 @@ var os = require('os'); var fs = require('fs'); var async = require('async'); var bcrypt = require('bcrypt-node'); +var dns = require('dns') var Args = require('pixl-args'); var Tools = require('pixl-tools'); @@ -155,6 +156,27 @@ for (var env_key in process.env) { } } +// helper function to resolve IPs for CRONICLE_cluster +const getIPsForHostnames = async (hostnames) => { + const ipPromises = hostnames.map(hostname => { + return new Promise((resolve, reject) => { + dns.lookup(hostname.trim(), (err, ip) => { + if (err) resolve({ hostname: hostname.trim(), ip: null }); + else resolve({ hostname: hostname.trim(), ip }); + }); + }); + }); + + try { + // Wait for all DNS lookups to finish + const ips = await Promise.all(ipPromises); + return ips; + } catch (error) { + console.error("Error fetching IP addresses:", error); + return []; + } +}; + // construct standalone storage server var storage = new StandaloneStorage(config.Storage, function (err) { if (err) throw err; @@ -188,12 +210,19 @@ var storage = new StandaloneStorage(config.Storage, function (err) { // make sure this is only run once // changing exit code to 0, so it won't break docker entry point - storage.get('global/users', function (err) { + storage.get('global/users', async function (err) { if (!err) { print("Storage has already been set up. There is no need to run this command again.\n\n"); process.exit(0); } + if(process.env['CRONICLE_cluster']) { + let servers = await getIPsForHostnames(process.env['CRONICLE_cluster'].split(',')) + servers.forEach(server =>{ + setup.storage.push(["listPush", "global/servers", server]) + }) + } + async.eachSeries(setup.storage, function (params, callback) { verbose("Executing: " + JSON.stringify(params) + "\n"); @@ -230,6 +259,34 @@ var storage = new StandaloneStorage(config.Storage, function (err) { }); break; + case 'reset': + + let newGroup = { regexp: '^(' + Tools.escapeRegExp(hostname) + ')$' } + + storage.listFindUpdate('global/server_groups', { id: "maingrp" }, newGroup, function (err) { + if (err) throw err; + print(`Main group regex is set to [ ${newGroup.regexp} ]`); + print("\n"); + + storage.listFind("global/servers", { hostname: hostname }, function (err, item) { + // already exist? + if (item) { + print(`${hostname} already exist in server list\n`); + storage.shutdown(function () { process.exit(1); }); + } + else { + storage.listPush("global/servers", { hostname: hostname, ip: ip }, function (err) { + if (err) throw err; + print(`Added ${hostname} to server list (remove old servers from UI as needed)\n`); + storage.shutdown(function () { process.exit(0); }); + }) + } + + }) + + }); + break; + case 'admin': // create or replace admin account // Usage: ./storage-cli.js admin USERNAME PASSWORD [EMAIL] diff --git a/htdocs/css/style.css b/htdocs/css/style.css index c25a957..3a8683d 100644 --- a/htdocs/css/style.css +++ b/htdocs/css/style.css @@ -650,7 +650,6 @@ td.table_label { tr.focus td { animation: 4s focus; - } body.dark span.color_label.gray { @@ -749,6 +748,10 @@ td.table_label { } span.red2 { + background-color: #d9667a; + } + + body.dark span.red2 { background-color: #ac394d; } @@ -762,7 +765,7 @@ td.table_label { } .wflog.grid-item { - border: 1px solid #ccc; + border: 0.1px solid rgba(48,48,48,1); padding: 10px; word-wrap: break-word; overflow: hidden; @@ -772,4 +775,315 @@ td.table_label { .wflog.grid-title { /* font-weight: bold; */ font-size: 1.6em; - } \ No newline at end of file + } + + .flex-container { + display: flex; + white-space: nowrap; + flex-wrap: nowrap; + justify-content: space-between; + /* align-items: center; */ + } + + .flex-container.widget { + align-items: center; + justify-content:space-between; + } + + .flex-container-stats { + display: flex; + white-space: nowrap; + flex-wrap: wrap; + justify-content: space-around; + /* align-items: center; */ + } + + .upcoming.grid-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 12px; + padding: 10px; + direction: ltr; + } + + .upcoming.schedule.grid-container { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + padding-top: 0px; + gap: 14px; + + } + + .stats.grid-container { + display: grid; + background-color: var(--box-background-color); + border: 1px solid var(--border-color); + border-right: 0px; + /* grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); */ + grid-template-columns: repeat(10, auto); + align-items: center; + gap: 0px; + padding: 0px; + direction: ltr; + /* background-color: #80808005; */ + /* border-radius: 6px; */ + + } + + .stats.grid-item { + border: 0px; + border-right: 1px solid var(--border-color); + /* border-top: 1px solid var(--border-color); */ + /* border-bottom: 1px solid var(--border-color); */ + background-color: var(--box-background-color); + /* background-color: #FAFAFA; */ + /* transition: border-width 0.3s; */ + padding: 6px; + border-radius: 0px; + /* border-top-left-radius: 6px; */ + /* border-bottom-left-radius: 6px; */ + word-wrap: break-word; + white-space: wrap; + overflow: hidden; + /* white-space: normal; */ + direction: ltr; + /* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */ + /* vertical-align: middle; */ + } + + /* body.dark .stats.grid-item { + border: 1px solid #cccccc26; + } + body.dark .stats.grid-item { + box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.75); + } */ + + body.dark .stats.grid-item:hover { + color: #FFF; + background-color: var(--background-color); + } + + .stats.grid-item:hover { + /* border: 1px solid #cccccc26; */ + background-color: #cccccc26; + } + + /* body.dark .stats.grid-item { + background-color: initial; + } + body.dark .stats.grid-item:hover { + color: #FFF; + background-color: initial; + } + .stats.grid-item:hover { + background-color: rgba(128,128,128, 0.1); + } */ + + @media (max-width: 1200px) { + .stats.grid-container { + grid-template-columns: repeat(5, 1fr); + } + } + + .job-details.grid-container { + display: grid; + /* grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); */ + grid-template-columns: repeat(7, auto); + align-items: center; + gap: 0px; + padding: 0px; + /* padding-bottom: 12px; */ + margin-top: 12px; + margin-bottom: 12px; + background-color: var(--background-color); + border: 1px solid var( --border-color); + border-bottom: 0px; + border-left: 0px; + border-radius: 0px; + } + + .job-details.grid-container.running { + grid-template-columns: repeat(6, auto); + } + + body.dark .job-details.grid-container { + background-color: var(--background-color); + } + + .job-details.grid-item { + display: flex; + align-items: baseline; + justify-content: space-evenly; + border-bottom: 1px solid var( --border-color); + border-left: 1px solid var( --border-color); + background-color: var(--background-color); + flex-wrap: wrap; + padding-top: 8px; + /* border-radius: 6px; */ + } + + body.dark .job-details.grid-item:hover div.info_value { + color: white; + text-shadow: none; + } + + .upcoming.grid-item { + border: 1px solid #ccccccde; + /* transition: border-width 0.3s; */ + padding: 8px; + border-radius: 12px; + word-wrap: break-word; + white-space: wrap; + overflow: hidden; + /* white-space: normal; */ + direction: ltr; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + /* vertical-align: middle; */ + } + + .upcoming.grid-item.focus { + animation: 4s focus; + } + + + body.dark .upcoming.schedule.grid-item { + background-color: #44444444 + /* color: #fff */ + } + + .upcoming.schedule.grid-item { + /* background-color: #fafafa */ + background-color: var(--background-color); + /* color: #fff */ + } + + .event-error { + color: #bb4444cc; + } + .event-success { + color: #44bb4460; + } + .event-warning { + color: #dbdb34bb; + } + .event-none { + color: #44444400; + } + + .section-divider { + grid-column: 1 / -1; + text-align: left; + /* color: initial; */ + font-size: 1.4em; + font-weight: bold; + } + + .upcoming.warning.grid-item { + background-color: #f5e05870;; + } + + .upcoming.error.grid-item { + background-color: pink; + } + + body.dark .upcoming.grid-item { + box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.75); + } + + .upcoming.minute.grid-item { + background-color: #b7ffb770 + } + + body.dark .upcoming.minute.grid-item { + background-color: #2c702cDD; + transition: background-color 3s; + color: #d3c7c7 + } + + .upcoming.grid-item a { + font-size: 1.1em; + } + + body.dark .upcoming.grid-item a { + color: #3a639b; + } + + body.dark .upcoming.minute.grid-item a { + color: #d3c7c7 !important; + font-size: 1.2em; + } + + body.dark .upcoming.soon.grid-item a { + color: #d3c7c7 !important; + font-size: 1.2em; + } + + .upcoming.minute.grid-item a { + color: rgb(84, 84, 84) !important; + font-size: 1.2em; + } + + .upcoming.soon.grid-item a { + color: rgb(84, 84, 84) !important; + font-size: 1.2em; + } + + .upcoming.soon.grid-item { + background-color: #f5e05870; + } + + body.dark .upcoming.soon.grid-item { + background-color: rgba(255, 115, 0, 0.7); + color: #d3c7c7; + } + + .upcoming.grid-item:hover { + border-color: rgba(128,128,128,0.5) + /* background-color:rgb(0, 255, 0) */ + } + + body.dark .upcoming.grid-item:hover:not(.schedule) { + /* border-color: rgba(46, 38, 12, 0.4); */ + color: #FFF; + } + + .event-action { + font-weight: bold; + color:#817c7c; + text-decoration: none; + } + + .event-action:hover { + text-decoration: underline; + } + + .search { + width : 100px; + background-color:gray; + -webkit-transition: all 0.3s linear; + -moz-transition: all 0.3s linear; + border-radius : 15px; + outline:none; + } + + body.dark .event-action:hover { + font-weight: bold; + color:#555; + } + + body.dark .upcoming.grid-item { + border: 1px solid #cccccc26; + } + + .upcoming.grid-title { + /* font-weight: bold; */ + font-size: 1.2em; + } + + .event-search { + padding-left:30px; + border:0.3px solid #ccccccaa; + border-radius:8px; + font-size: 1.2em; + } + + diff --git a/htdocs/js/app.js b/htdocs/js/app.js index 1c9f46b..4ba5340 100644 --- a/htdocs/js/app.js +++ b/htdocs/js/app.js @@ -819,6 +819,18 @@ function get_pretty_int_list(arr, ranges) { return arr.slice(0, arr.length - 1).join(', ') + ' and ' + arr[ arr.length - 1 ]; } +function summarize_event_timing_short(timing) { + if(!timing) return "On Demand" + let type = 'Hourly' + let total = (timing.minutes || []).length || 60 + if(timing.hours) { total = total*(timing.hours.length || 24); type = 'Daily'} else { total = total*24} + if(timing.weekdays) { total = total*(timing.weekdays.length || 1); type = 'Weekly'} + if(timing.days) { total = total*(timing.days.length || 1); type = 'Monthly'} + if(timing.months) { total = total*(timing.months.length || 1); type = 'Yearly'} + if(timing.years) { total = total*(timing.years.length || 1); type = 'Custom'} + return `${type} :: ${total}` +} + function summarize_event_timing(timing, timezone, extra) { // summarize event timing into human-readable string if (!timing && extra) { diff --git a/htdocs/js/pages/Base.class.js b/htdocs/js/pages/Base.class.js index 192f69a..1a8bc59 100644 --- a/htdocs/js/pages/Base.class.js +++ b/htdocs/js/pages/Base.class.js @@ -60,18 +60,36 @@ Class.subclass(Page, "Page.Base", { return '
 ' + id + '
'; }, - getNiceEvent: function (title, width, style, extra) { + getNiceEvent: function (title, width, style, extra, extraTooltip) { if (!width) width = 500; if (!title) return '(None)'; if (!style) style = ''; if (!extra) extra = ''; + let tooltip = title.notes ? title.notes.replace(/\"/g, """) : "" + if(extraTooltip) { + let cat = title.category_title || '(none)' + let plug = title.plugin_title || '(none)' + let target = title.group_title || '(none)' + tooltip = `Category: ${cat}
Plugin: ${plug}
Target: ${target}
Notes:

${tooltip}` + } + let icon_class = 'fa fa-clock-o'; if(title.plugin == 'workflow') icon_class = 'fa fa-folder'; - let notes = title.notes ? title.notes.replace(/\"/g, """) : "" + + let icon = ` ` + + if (extraTooltip) { + if (title.plugin == 'dockerplug') icon = `` + if (title.plugin == 'shellplug') icon = `` + if (title.plugin == 'sshxplug' || title.plugin == 'sshplug') icon = `` + if (title.plugin == 'urlplug') icon = `` + } + if (typeof (title) == 'object') { title = title.title } - return `
 ${title}${extra}
`; + + return `
${icon} ${title}${extra}
`; }, getNiceCategory: function (cat, width, collapse) { diff --git a/htdocs/js/pages/History.class.js b/htdocs/js/pages/History.class.js index 4cb5bc7..34672d7 100644 --- a/htdocs/js/pages/History.class.js +++ b/htdocs/js/pages/History.class.js @@ -300,6 +300,13 @@ Class.subclass( Page.Base, "Page.History", { if (!args.limit) args.limit = 50; app.api.post( 'app/get_event_history', copy_object(args), this.receive_event_stats.bind(this) ); }, + + togglePerfLegend: function() { + let chart = this.charts.perf + if(!chart) return; + chart.options.legend.display = !chart.options.legend.display + chart.update() + }, receive_event_stats: function(resp) { // render event stats page @@ -335,32 +342,11 @@ Class.subclass( Page.Base, "Page.History", { ['error_history', "Query History"], ] ); - html += '
'; + // html += '
'; + + let eventTitle = `${this.getNiceEvent(event.title, col_width)}` - html += '
Event Stats'; - - html += '
'; - html += '
EVENT NAME
'; - html += ''; - - html += '
CATEGORY NAME
'; - html += '
' + this.getNiceCategory(cat, col_width) + '
'; - - html += '
EVENT TIMING
'; - html += '
' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '
'; - html += '
'; - - html += '
'; - html += '
USERNAME
'; - html += '
' + this.getNiceUsername(event, false, col_width) + '
'; - - html += '
PLUGIN NAME
'; - html += '
' + this.getNicePlugin(plugin, col_width) + '
'; - - html += '
EVENT TARGET
'; - html += '
' + this.getNiceGroup(group, event.target, col_width) + '
'; - html += '
'; - + var total_elapsed = 0; var total_cpu = 0; var total_mem = 0; @@ -391,35 +377,29 @@ Class.subclass( Page.Base, "Page.History", { } } - html += '
'; - html += '
AVG. ELAPSED
'; - html += '
' + get_text_from_seconds(total_elapsed / count, true, false) + '
'; - - html += '
AVG. CPU
'; - html += '
' + short_float(total_cpu / count) + '%
'; - - html += '
AVG. MEMORY
'; - html += '
' + get_text_from_bytes( total_mem / count ) + '
'; - html += '
'; - - html += '
'; - html += '
SUCCESS RATE
'; - html += '
' + pct(total_success, count) + '
'; - - html += '
LAST RESULT
'; - html += '
' + nice_last_result + '
'; - - html += '
AVG. LOG SIZE
'; - html += '
' + get_text_from_bytes( total_log_size / count ) + '
'; - html += '
'; - - html += '
'; - html += '
'; + html += ` +
+
EVENT NAME:
${eventTitle}
+
CATEGORY:
${this.getNiceCategory(cat, col_width) }
+
PLUGIN:
${this.getNicePlugin(plugin, col_width)}
+
EVENT TARGET:
${this.getNiceGroup(group, event.target, col_width)}
+
USERNAME:
${this.getNiceUsername(event, false, col_width)}
+
EVENT TIMING:
${event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)'}
+ +
AVG CPU:
${short_float(total_cpu / count)}%
+
AVG MEMORY:
${get_text_from_bytes( total_mem / count )}
+
AVG LOG SIZE:
${get_text_from_bytes( total_log_size / count )}
+
AVG ELAPSED:
${get_text_from_seconds(total_elapsed / count, true, false)}
+
SUCCESS RATE:
${pct(total_success, count)}
+
LAST RESULT:
${nice_last_result}
+
+
+ ` // graph containers html += '
'; - html += '
Performance History
'; - html += '
'; + html += '
Performance History
'; + html += `
`; // $P().togglePerfLegend()` html += '
'; html += '
'; @@ -550,11 +530,14 @@ Class.subclass( Page.Base, "Page.History", { labels: { fontStyle: 'bold', padding: 15 - } + }, + }, + + title:{ display: false, - text: "" + text: "toggle legend" }, scales: { xAxes: [{ @@ -576,13 +559,16 @@ Class.subclass( Page.Base, "Page.History", { callback: function(value, index, values) { if (value < 0) return ''; return '' + get_text_from_seconds_round_custom(value, true); - } + }, + onClick: function(e,i) {$P().togglePerfLegend()} }, scaleLabel: { display: true, + onClick: $P().togglePerfLegend // labelString: 'value' } - }] + }], + }, tooltips: { mode: 'nearest', diff --git a/htdocs/js/pages/Home.class.js b/htdocs/js/pages/Home.class.js index e1cc214..596c8c0 100644 --- a/htdocs/js/pages/Home.class.js +++ b/htdocs/js/pages/Home.class.js @@ -8,14 +8,14 @@ Class.subclass( Page.Base, "Page.Home", { this.worker.onmessage = this.render_upcoming_events.bind(this); this.div.html(` +
-
- - +
+
@@ -57,7 +57,6 @@ Class.subclass( Page.Base, "Page.Home", {
- +
`) }, @@ -110,9 +110,16 @@ Class.subclass( Page.Base, "Page.Home", { var html = ''; html += 'Upcoming Events'; - html += '
 
'; + html += '
 
'; - html += `
 
`; + let ue_options = [ + {title: "Grid View", id: "grid"}, + {title: "Compact View", id: "compact"}, + {title: "Show All", id: "all"} + ] + let up_event_select = render_menu_options( ue_options , app.getPref('fe_up_eventlimit') || 'grid', false ) + '' + + html += `
 ' + this.render_target_menu_options( args.target ) + '
'; html += '
 
'; html += '
 
'; @@ -176,28 +183,36 @@ Class.subclass( Page.Base, "Page.Home", { let errBg = stats.jobs_completed > 0 && (stats.jobs_failed || 0)/stats.jobs_completed > (parseFloat(ui.err_rate) || 0.03) ? 'red2' : 'gray' let errTitle = Object.entries(stats.errorLog || {}).slice(0,20).sort((a,b)=> a[1] < b[1] ? 1 : -1).map(e=>`${e[0]}:\t${e[1]}`).join("\n") + // xhtml - html += ` -
Server Stats -
EVENTS:  ${ active_events.length} 
-
CATEGORIES:  ${app.categories.length} 
-
PLUGINS:  ${app.plugins.length} 
-
JOBS COMPLETED TODAY:  ${stats.jobs_completed || 0 } 
-
FAILED:  - ${stats.jobs_failed || 0}  -
-
SUCCESS RATE:  ${pct( (stats.jobs_completed || 0) - (stats.jobs_failed || 0), stats.jobs_completed || 1 ) } 
-
AVG LOG SIZE:  2K 
- -
MANAGER UPTIME:  ${get_text_from_seconds( mserver.uptime || 0, false, true )} 
-
CPU:  ${short_float(total_cpu)}% 
-
MEMORY:  ${get_text_from_bytes(total_mem)} 
-
SERVERS:  ${num_keys(servers)} 
-
- ` - - // $('#d_home_header_stats').html(html); - document.getElementById('d_home_header_stats').innerHTML = html; + let failed_badge = `${stats.jobs_failed || 0} ` + + status_bar = [ + {name: "EVENTS", value: active_events.length}, + {name: "CATS", value: app.categories.length}, + // {name: "PLUGINS", value: app.plugins.length}, + {name: "JOBS", value: stats.jobs_completed || 0}, + {name: "FAILED", value: failed_badge}, + {name: "SUCCESS", value: pct( (stats.jobs_completed || 0) - (stats.jobs_failed || 0), stats.jobs_completed || 1 )}, + {name: "LOG SIZE", value: get_text_from_bytes((stats.jobs_log_size || 0) / (stats.jobs_completed || 1))}, + {name: "UPTIME", value: get_text_from_seconds( mserver.uptime || 0, false, true )}, + {name: "CPU", value: `${short_float(total_cpu)}%`}, + {name: "MEMORY", value: get_text_from_bytes(total_mem)}, + {name: "SERVERS", value: num_keys(servers)} + ] + + html = '
' + status_bar.forEach(e=>{ + html += `
+
${e.name}: 
+
 ${e.value} 
+
+ ` + }) + + html += "
" + + document.getElementById('d_home_header_stats').innerHTML = html; }, refresh_upcoming_events: function() { @@ -214,6 +229,7 @@ Class.subclass( Page.Base, "Page.Home", { nav_upcoming: function(offset) { // refresh upcoming events with new offset + app.setPref('fe_up_eventlimit', document.getElementById('fe_up_eventlimit').value) this.upcoming_offset = offset; this.render_upcoming_events({ data: this.upcoming_events @@ -248,6 +264,7 @@ Class.subclass( Page.Base, "Page.Home", { this.upcoming_events = e.data; var viewType = document.getElementById('fe_up_eventlimit').value; + let isGrid = viewType === 'grid' // apply filters var events = []; @@ -259,7 +276,7 @@ Class.subclass( Page.Base, "Page.Home", { var stub = e.data[idx]; var item = find_object( app.schedule, { id: stub.id } ) || {}; - if (viewType == "Compact View") { // one row per event, use badge for job count + if (viewType == "compact" || isGrid) { // one row per event, use badge for job count var currSched = moment.tz(stub.epoch * 1000, item.timezone || app.tz).format("h:mm A z"); var currCD = get_text_from_seconds_round(Math.max(60, stub.epoch - now), false); @@ -291,12 +308,14 @@ Class.subclass( Page.Base, "Page.Home", { events.push( stub ); } // foreach item in schedule + + let xhtml = '' var size = get_inner_window_size(); var col_width = Math.floor( ((size.width * 0.9) + 50) / 7 ); var cols = ['Event Name', 'Category', 'Plugin', 'Target', 'Scheduled Time', 'Countdown', 'Actions']; - var limit = 25; + var limit = Math.round((window.innerWidth)/350)*4 // try to fit 4 rows html += this.getPaginatedTable({ resp: { @@ -305,7 +324,7 @@ Class.subclass( Page.Base, "Page.Home", { length: events.length } }, - cols: cols, + cols: isGrid ? [] : cols, data_type: 'pending event', limit: limit, offset: this.upcoming_offset, @@ -327,6 +346,7 @@ Class.subclass( Page.Base, "Page.Home", { var nice_countdown = 'Now'; if (stub.epoch > now) { nice_countdown = get_text_from_seconds_round( Math.max(60, stub.epoch - now), false ); + nice_countdown_short = get_text_from_seconds_round( Math.max(60, stub.epoch - now), true ).replace(' hr', 'h'); } if (group && item.multiplex) { @@ -334,19 +354,33 @@ Class.subclass( Page.Base, "Page.Home", { group.multiplex = 1; } - var badge = ''; - if(viewType == "Compact View") { + let badge = ''; + if(viewType == "compact" || isGrid) { var overLimitRows = stubCounter[stub.id] > maxSchedRows ? ` + ${stubCounter[stub.id] - maxSchedRows} more` : ''; var scheduleList = stubTitle[stub.id] + `${overLimitRows}` var jobCount = stubCounter[stub.id] - if(jobCount < 10) jobCount = ` ${jobCount} `; - var badge = `${jobCount}`; + if(jobCount < 10 ) jobCount = ` ${jobCount} `; + badge = `${jobCount}`; } - var eventName = self.getNiceEvent('' + item.title + '', col_width, 'float:left', '  ') + let extraSpace = isGrid ? ' ' : '  ' + let eventName = self.getNiceEvent('' + item.title + '', col_width, 'float:left', extraSpace) + + if(isGrid) { + + badge = stubCounter[stub.id] > 1 ? `+${stubCounter[stub.id]-1}` : '' + + // turn minute/hour to min/h if event name is too long + if(Math.max(60, stub.epoch - now) <= 60*5) { // for minute/soon (larger font) + nice_countdown = `
${item.title.length > 18 ? nice_countdown_short : nice_countdown}
` + } + else { // regular + nice_countdown = `
${item.title.length > 26 ? nice_countdown_short : nice_countdown}
` + } + } var tds = [ - ` ${eventName} ${badge} `, + ` ${eventName}${badge}`, self.getNiceCategory( cat, col_width ), self.getNicePlugin( plugin, col_width ), self.getNiceGroup( group, item.target, col_width ), @@ -362,11 +396,34 @@ Class.subclass( Page.Base, "Page.Home", { } if(!app.state.enabled) tds.className += ' disabled' - - return tds; + + if (isGrid) { + let proximity = '' + if (stub.epoch - now <= 60*5) proximity = 'soon' + if (stub.epoch - now <= 60) proximity = 'minute' + + xhtml += ` +
+
+
${tds[0]}
+
${nice_countdown}
+
+
+ ` + //
+ + } + else { // compact table + return tds + } + + + } // row callback }); // table - + + $('#upcoming_grid').html(xhtml) + // $('#d_home_upcoming_events').removeClass('loading').html( html ); let upcoming = document.getElementById('d_home_upcoming_events'); upcoming.classList.remove('loading'); @@ -406,19 +463,27 @@ Class.subclass( Page.Base, "Page.Home", { } let isDark = app.getPref('theme') === 'dark' - let green = isDark ? '#44bb44' : 'lightgreen' // success - let orange = isDark ? '#bbbb44' : 'orange' // warning - let red = isDark ? '#bb4444' : 'pink' // error + let green = isDark ? '#44bb44DD' : '#90EE90AA' // success + let orange = isDark ? '#bbbb44DD' : '#FFA500AA' // warning + let red = isDark ? '#bb4444DD' : '#F88379AA' // error let statusMap = { 0: green, 255: orange } let labels = jobs.map(e => '') - if(jobLimit < 100) labels = jobs.map((j, i) => i == 0 ? j.event_title.substring(0, 4) : j.event_title); + if(jobLimit <= 100) labels = jobs.map((j, i) => i == 0 ? j.event_title.substring(0, 4) : j.event_title); + + let ctx = document.getElementById('d_home_completed_jobs'); + // var gradient = ctx.createLinearGradient(0, 0, 0, 400); + // gradient.addColorStop(0, 'rgba(250,174,50,1)'); + // gradient.addColorStop(1, 'rgba(250,174,50,0)'); + + let datasets = [{ label: 'Completed Jobs', // data: jobs.map(j => Math.ceil(j.elapsed/60)), data: jobs.map(j => Math.ceil(j.elapsed) + 1), backgroundColor: jobs.map(j => statusMap[j.code] || red), - jobs: jobs + jobs: jobs, + // borderWidth: 0.3 }]; let scaleType = document.getElementById('fe_cmp_job_chart_scale').value || 'logarithmic'; @@ -434,8 +499,7 @@ Class.subclass( Page.Base, "Page.Home", { return } - let ctx = document.getElementById('d_home_completed_jobs'); - + app.jobHistoryChart = new Chart(ctx, { type: 'bar', data: { @@ -446,8 +510,10 @@ Class.subclass( Page.Base, "Page.Home", { options: { legend: { display: false }, + animation: {duration: 0}, layout: { padding: { bottom: jobLimit > 50 ? 50 : 20 } }, tooltips: { + yAlign: 'top', titleFontSize: 14, titleFontColor: 'orange', @@ -474,7 +540,7 @@ Class.subclass( Page.Base, "Page.Home", { }], yAxes: [{ type: scaleType, - gridLines: { color: 'rgb(170, 170, 170)', lineWidth: 0.3 }, + gridLines: { color: 'rgb(170, 170, 170)', lineWidth: 0.3 }, ticks: { display: false, beginAtZero: true, @@ -510,16 +576,22 @@ Class.subclass( Page.Base, "Page.Home", { for (var id in app.activeJobs) { jobs.push( app.activeJobs[id] ); } + + if(jobs.length === 0) return '
No active jobs found
' // sort events by time_start descending this.jobs = jobs.sort( function(a, b) { - return (a.time_start < b.time_start) ? 1 : -1; + if(a.plugin === 'workflow') return -1 + if(b.plugin === 'workflow') return 1 + return (a.time_start - b.time_start) + // return (a.time_start > b.time_start) ? 1 : -1; } ); - var cols = ['Job ID', 'Event Name', 'Argument', 'Category', 'Hostname', 'Elapsed', 'Progress', 'Remaining', 'Performance', 'Memo', 'Actions']; + var cols = this.jobs.length > 0 ? ['Job ID', 'Event Name', 'Argument', 'Category', 'Hostname', 'Elapsed', 'Progress', 'Remaining', 'Performance', 'Memo', 'Actions'] : []; // render table var self = this; + html += this.getBasicTable( this.jobs, cols, 'active job', function(job, idx) { var actions = [ // 'Details', diff --git a/htdocs/js/pages/JobDetails.class.js b/htdocs/js/pages/JobDetails.class.js index b8444fa..7f87720 100644 --- a/htdocs/js/pages/JobDetails.class.js +++ b/htdocs/js/pages/JobDetails.class.js @@ -209,7 +209,7 @@ Class.subclass(Page.Base, "Page.JobDetails", { html += 'Completed Job'; if (event.id && !event.multiplex) html += '
 Run Again
'; - if (event.id) html += ``; + let jumpToHist = ``; //adding edit button on job detail page if (event.id) html += ''; if (app.isAdmin()) html += '
 Delete Job
'; @@ -222,86 +222,60 @@ Class.subclass(Page.Base, "Page.JobDetails", { html += this.get_job_result_banner(job).replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ""); // fieldset header - html += '
Job Details'; + html += '
Job Details'; - // if (event.id && !event.multiplex) html += '
Run Again
'; + let eventTitle = '(None)' + if (event.id) eventTitle = '' + this.getNiceEvent(job.event_title, col_width) + ''; + else if (job.event_title) eventTitle = this.getNiceEvent(job.event_title, col_width); - html += '
'; - html += '
JOB ID
'; - html += '
' + job.id + '
'; + let jobCategory = '(None)' + if (cat) jobCategory = this.getNiceCategory(cat, col_width); + else if (job.category_title) jobCategory= this.getNiceCategory({ title: job.category_title }, col_width); - html += '
EVENT NAME
'; - html += '
'; - if (event.id) html += '' + this.getNiceEvent(job.event_title, col_width) + ''; - else if (job.event_title) html += this.getNiceEvent(job.event_title, col_width); - else html += '(None)'; - html += '
'; - - // html += '
EVENT TIMING
'; - // html += '
' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '
'; - // html += '
'; - - html += '
ARGUMENT
'; // hist - html += '
' + encode_entities(job.arg || '(no argument)') + '
'; - html += '
'; + let jobPlugin = '(None)' + if (plugin) jobPlugin = this.getNicePlugin(plugin, col_width); + else if (job.plugin_title) jobPlugin = this.getNicePlugin({ title: job.plugin_title }, col_width); - html += '
'; - html += '
CATEGORY NAME
'; - html += '
'; - if (cat) html += this.getNiceCategory(cat, col_width); - else if (job.category_title) html += this.getNiceCategory({ title: job.category_title }, col_width); - else html += '(None)'; - html += '
'; - - html += '
PLUGIN NAME
'; - html += '
'; - if (plugin) html += this.getNicePlugin(plugin, col_width); - else if (job.plugin_title) html += this.getNicePlugin({ title: job.plugin_title }, col_width); - else html += '(None)'; - html += '
'; + let jobTarget = '(None)' + if (group || event.target) jobTarget = this.getNiceGroup(group, event.target, col_width); + else if (job.nice_target) jobTarget = '
' + job.nice_target + '
'; - html += '
EVENT TARGET
'; - html += '
'; - if (group || event.target) html += this.getNiceGroup(group, event.target, col_width); - else if (job.nice_target) html += '
' + job.nice_target + '
'; - else html += '(None)'; - html += '
'; - html += '
'; - - html += '
'; - html += '
JOB SOURCE
'; - html += `
` + (job.source || 'Scheduler') + '
'; - - html += '
SERVER HOSTNAME
'; - html += '
' + this.getNiceGroup(null, job.hostname, col_width) + '
'; - - html += '
PROCESS ID
'; - html += '
' + (job.detached_pid || job.pid || '(Unknown)') + '
'; - html += '
'; - - html += '
'; - html += '
JOB STARTED
'; - html += '
'; + let jobStarted = get_nice_date_time(job.time_start, false, true); if ((job.time_start - job.now >= 60) && !event.multiplex && !job.source) { - html += ''; - html += get_nice_date_time(job.time_start, false, true); - html += ''; + jobStarted = `${get_nice_date_time(job.time_start, false, true)}` } - else html += get_nice_date_time(job.time_start, false, true); - html += '
'; - html += '
JOB COMPLETED
'; - html += '
' + get_nice_date_time(job.time_end, false, true) + '
'; + html += ''; - html += '
ELAPSED TIME
'; - html += '
' + get_text_from_seconds(job.elapsed, false, false) + '
'; - html += '
'; + let timing = summarize_event_timing(event.timing, event.timezone) + + html += ` +
+
JOB ID:
${job.id}
+
PID:
${(job.detached_pid || job.pid || '(Unknown)')}
+
CAT:
${jobCategory}
+
SOURCE:
${job.source || 'Scheduler'}
+
TARGET:
${jobTarget}
+
START:
${jobStarted}
+
ELAPSED:
${get_text_from_seconds(job.elapsed, false, false)}
+ + +
EVENT:
${eventTitle}
+
ARG:
${encode_entities(job.arg || '(no argument)')}
+
PLUGIN:
${jobPlugin}
+
MEMO:
${job.memo || '(none)'}
+
HOST:
${this.getNiceGroup(null, job.hostname, col_width)}
+
END:
${get_nice_date_time(job.time_end, false, true)}
+
${jumpToHist }
+ +
+
+ ` - html += '
'; - html += ''; + //
${timing}
// pies - html += '
'; + html += '
'; html += '
'; html += '
Performance Metrics
'; @@ -825,76 +799,36 @@ Class.subclass(Page.Base, "Page.JobDetails", { html += '
'; html += '
'; - // fieldset header - html += '
Job Details'; - - // html += '
Abort Job...
'; - - html += '
'; - html += '
JOB ID
'; - html += ``; - - html += '
EVENT NAME
'; - html += ''; - - // html += '
EVENT TIMING
'; - // html += '
' + (event.enabled ? summarize_event_timing(event.timing, event.timezone) : '(Disabled)') + '
'; - // html += '
'; - - html += '
ARGUMENT
'; // hist - html += '
' + encode_entities(job.arg || '(no argument)') + '
'; - html += '
'; - - // html += '
ARGUMENT
'; - // html += '
' + encode_entities(job.arg || '') + '
'; - // html += '
'; - - - html += '
'; - html += '
CATEGORY NAME
'; - html += '
' + this.getNiceCategory(cat, col_width) + '
'; - - html += '
PLUGIN NAME
'; - html += '
' + this.getNicePlugin(plugin, col_width) + '
'; - - html += '
EVENT TARGET
'; - html += '
' + this.getNiceGroup(group, event.target, col_width) + '
'; - html += '
'; - - html += '
'; - html += '
JOB SOURCE
'; - html += `
' + (job.source || 'Scheduler') + '
'; - - html += '
SERVER HOSTNAME
'; - html += '
' + this.getNiceGroup(null, job.hostname, col_width) + '
'; - - html += '
PROCESS ID
'; - html += '
' + job.pid + '
'; - html += '
'; - - html += '
'; - html += '
JOB STARTED
'; - html += '
' + get_nice_date_time(job.time_start, false, true) + '
'; - - html += '
ELAPSED TIME
'; - var elapsed = Math.floor(Math.max(0, app.epoch - job.time_start)); - html += '
' + get_text_from_seconds(elapsed, false, false) + '
'; - - var progress = job.progress || 0; - var nice_remain = 'n/a'; + let eventTitle = `${this.getNiceEvent(job.event_title, col_width)}` + let elapsed = Math.floor(Math.max(0, app.epoch - job.time_start)); + let job_progress = job.progress || 0; + let nice_remain = 'n/a'; if (job.pending && job.when) { nice_remain = 'Retry in ' + get_text_from_seconds(Math.max(0, job.when - app.epoch), true, true) + ''; } - else if ((elapsed >= 10) && (progress > 0) && (progress < 1.0)) { - var sec_remain = Math.floor(((1.0 - progress) * elapsed) / progress); + else if ((elapsed >= 10) && (job_progress > 0) && (job_progress < 1.0)) { + var sec_remain = Math.floor(((1.0 - job_progress) * elapsed) / job_progress); nice_remain = get_text_from_seconds(sec_remain, false, true); } - html += '
REMAINING TIME
'; - html += '
' + nice_remain + '
'; - html += '
'; - html += '
'; - html += ''; + html += ` +
+
JOB ID:
${job.id}
+
PID:
${(job.detached_pid || job.pid || '(Unknown)')}
+
CAT:
${this.getNiceCategory(cat, col_width)}
+
TARGET:
${this.getNiceGroup(group, event.target, col_width) }
+
SOURCE:
${job.source || 'Scheduler'}
+
START:
${get_nice_date_time(job.time_start, false, true) }
+ +
EVENT:
${eventTitle}
+
ARG:
${encode_entities(job.arg || '(no argument)')}
+
PLUGIN:
${this.getNicePlugin(plugin, col_width)}
+
HOST:
${this.getNiceGroup(null, job.hostname, col_width)}
+
ELAPSED TIME:
${get_text_from_seconds(elapsed, false, false)}
+
REMAINING TIME:
${nice_remain}
+
+
+ ` // pies html += '
'; diff --git a/htdocs/js/pages/Schedule.class.js b/htdocs/js/pages/Schedule.class.js index ba1a8d7..5339077 100644 --- a/htdocs/js/pages/Schedule.class.js +++ b/htdocs/js/pages/Schedule.class.js @@ -122,7 +122,7 @@ Class.subclass(Page.Base, "Page.Schedule", { py: "python" }, - setFileEditor: function(fileName) { + setFileEditor: function (fileName) { const self = this let editor = CodeMirror.fromTextArea(document.getElementById("fe_ee_pp_file_content"), { mode: self.extension_map[fileName.split('.').pop()] || 'text', @@ -136,9 +136,9 @@ Class.subclass(Page.Base, "Page.Schedule", { lint: true }) - editor.on('change', function(cm){ + editor.on('change', function (cm) { document.getElementById("fe_ee_pp_file_content").value = cm.getValue(); - }); + }); editor.setSize('52vw', '52vh') @@ -180,7 +180,7 @@ Class.subclass(Page.Base, "Page.Schedule", { get_form_table_row('Name', ``) + get_form_table_spacer() + get_form_table_row('Content', ``) - html +=`` + html += `` setTimeout(() => self.setFileEditor('.text'), 30) // editor needs to wait for a bit for modal window to render @@ -224,7 +224,7 @@ Class.subclass(Page.Base, "Page.Schedule", { get_form_table_row('Name', ``) + get_form_table_spacer() + get_form_table_row('Content', ``) - html += '' + html += '' setTimeout(() => self.setFileEditor(file.name), 30) // editor needs to wait for a bit for modal window to render @@ -621,6 +621,12 @@ Class.subclass(Page.Base, "Page.Schedule", { }, + show_event_stats: function(id) { + // let evt = find_object(app.schedule, {id: id}) + // document.getElementById('fe_event_info').innerHTML = `${evt.title}: category: ${evt.category} , plugin: ${evt.plugin}` + // $('#ex_' + id).toggle() + }, + gosub_events: function (args) { // render table of events with filters and search this.div.removeClass('loading'); @@ -750,29 +756,36 @@ Class.subclass(Page.Base, "Page.Schedule", { } // Scheduled Event page: + let miniButtons = '' - let miniButtons = '
' + if (app.hasPrivilege('create_events')) { + miniButtons += '
' + miniButtons += '
' + } if (app.isAdmin()) { - miniButtons += '
' + miniButtons += '
' } - if (app.hasPrivilege('create_events')) { - miniButtons += '
' - miniButtons += '
' - } + miniButtons += '
' + + let eventView = app.getPref('event_view') || 'details' + let isGrid = eventView === 'grid' || eventView === 'gridall' html += ` -
-
Scheduled Events ${cycleWarning} -
 
-
 
-
 
-
 
-
 
- ${miniButtons} -
+
+
Scheduled Events ${cycleWarning}
+
${miniButtons}
+
+
 
+
 
+
 
+
 
+
 
+
+
+
` // prep events for sort this.events.forEach(function (item) { @@ -780,6 +793,8 @@ Class.subclass(Page.Base, "Page.Schedule", { var group = item.target ? find_object(app.server_groups, { id: item.target }) : null; var plugin = item.plugin ? find_object(app.plugins, { id: item.plugin }) : null; + if(item.enabled && cat.enabled) item.active = true + item.category_title = cat ? cat.title : 'Uncategorized'; item.group_title = group ? group.title : item.target; item.plugin_title = plugin ? plugin.title : 'No Plugin'; @@ -799,29 +814,57 @@ Class.subclass(Page.Base, "Page.Schedule", { } // header center (group by buttons) + cols.headerRight = `
+ - +
` + // searchBar + cols.headerCenter = `
 
` + // render table - var last_group = ''; - - var htmlTab = this.getBasicTable2(this.events, cols, 'event', function (item, idx) { - var actions = [ - 'Run', - 'Edit', - 'Stats', - 'History', - 'Delete', - // 'Delete' - ]; + let last_group = ''; + + let xhtml = ''; + + let events = this.events || []; + + let totalEvents = events.length + + if(eventView === 'grid') { + totalEvents = `${events.filter(e=>e.active).length} active` + } + + var htmlTab = this.getBasicTable2(events, cols, 'event', function (item, idx) { + + let actions; + + if (isGrid) { + actions = [ + 'run', + `history`, + 'delete' + ] + + } + else { + actions = [ + 'Run', + 'Edit', + 'Stats', + 'History', + 'Delete', + // 'Delete' + ]; + } var cat = item.category ? find_object(app.categories, { id: item.category }) : null; var group = item.target ? find_object(app.server_groups, { id: item.target }) : null; @@ -854,7 +897,8 @@ Class.subclass(Page.Base, "Page.Schedule", { if (item.chain_error && !app.event_map[item.chain_error]) chain_error += '' + item.chain_error + '
'; var chain_error_msg = chain_error ? ` ` : ''; - var evt_name = self.getNiceEvent(item, col_width, 'float:left', '  '); + var evt_name = self.getNiceEvent(item, col_width, 'float:left', '  ', isGrid); + if (chain_tooltip.length > 0) evt_name += `  ${chain_error_msg}`; // check if event is has limited time range @@ -862,22 +906,22 @@ Class.subclass(Page.Base, "Page.Schedule", { if (item.start_time && Number(item.start_time) > new Date().valueOf() + 60000) inactiveTitle = 'Schedule will resume at ' + new Date(item.start_time).toLocaleString() if (item.end_time && Number(item.end_time) < new Date().valueOf()) inactiveTitle = 'Schedule expired on ' + new Date(item.end_time).toLocaleString() - let niceTiming = summarize_event_timing(item.timing, item.timezone, !inactiveTitle ? item.ticks : null) + let niceTiming = summarize_event_timing(item.timing, item.timezone, (inactiveTitle || isGrid) ? null : item.ticks) if (inactiveTitle) { niceTiming = `${niceTiming}` - if (item.ticks) niceTiming += ` + ` + if (item.ticks) niceTiming += ` + ` } let now = Date.now() / 1000 - - var tds = [ + + tds = [ '', - '
' + evt_name + '
', + `
' + evt_name + '
', self.getNiceCategory(cat, col_width), self.getNicePlugin(plugin, col_width), self.getNiceGroup(group, item.target, col_width), - niceTiming + chainInfo, + niceTiming + (isGrid ? '' : chainInfo), '' + status_html + '', get_text_from_seconds(now - item.modified, true, true), //modified actions.join(' | ') @@ -894,38 +938,109 @@ Class.subclass(Page.Base, "Page.Schedule", { tds.className += cat.color; } + // group by if (group_by) { + let cur_group = item[group_by + '_title']; tds.className = 'event_group_' + (group_by == 'group' ? item['target'] || 'allgrp' : item[group_by]) + ' ' + tds.className if (cur_group != last_group) { last_group = cur_group; - var insert_html = '
'; - switch (group_by) { - case 'category': insert_html += self.getNiceCategory(cat, 500, args.collapse); break; - case 'plugin': insert_html += self.getNicePlugin(plugin, 500, args.collapse); break; - case 'group': insert_html += self.getNiceGroup(group, item.target, 500, args.collapse); break; + let group_title; + + if (isGrid) { // grid view + switch (group_by) { + case 'category': group_title = self.getNiceCategory(cat, 500, args.collapse); break; + case 'plugin': group_title = self.getNicePlugin(plugin, 500, args.collapse); break; + case 'group': group_title = self.getNiceGroup(group, item.target, 500, args.collapse); break; + } + + // for regular grid - do not show disabled category + if(eventView === 'grid' && group_by === 'category' && !cat.enabled) group_title = null; + + if (group_title) xhtml += `
${group_title}
` + // tds.insertAbove = group_title; + } + else { // table view + let insert_html = '
'; + switch (group_by) { + case 'category': insert_html += self.getNiceCategory(cat, 500, args.collapse); break; + case 'plugin': insert_html += self.getNicePlugin(plugin, 500, args.collapse); break; + case 'group': insert_html += self.getNiceGroup(group, item.target, 500, args.collapse); break; + } + tds.insertAbove = `${insert_html}
`; } - insert_html += '
'; - tds.insertAbove = insert_html; + } // group changed + if (args.collapse) tds.hide = true } // group_by + + let timingTitle = niceTiming; + let timing = niceTiming.length > 20 ? summarize_event_timing_short(item.timing) : tds[5] + + + if(item.ticks) { + timingTitle += `

Extra ticks: ${item.ticks}` + timing += "+" + } + + if(app.chained_jobs[item.id]) { + timingTitle += ('

' + app.chained_jobs[item.id]) + timing += "<"; + } + + let lastStatus = 'event-none' + let jobCodes = app.state.jobCodes || {} + let xcode = jobCodes[item.id]; + if (xcode === 0) lastStatus = 'event-success' + if (xcode > 0) lastStatus = 'event-error' + if (xcode === 255) lastStatus = 'event-warning' + + // ${tds[0]} + //
+ let itemVisibility = eventView === 'grid' && (!item.active || args.collapse) ? 'none' : 'true' + // link item to it's group, avoid for disabled event on basic grid view + let itemClass = eventView === 'grid' && !item.active ? '' : tds.className + + xhtml += ` +
+
+
${tds[1]}
+ +
+
+ +
+
${actions.join(' | ')}
+
+ ${timing} +
+
+
+ + ` return tds; }); + if (isGrid) html += ` +
+
${totalEvents} events
+ ${cols.headerCenter} +
${cols.headerRight}
+
+
${xhtml}
` + else html += `
${htmlTab}
` + // table and graph (hide latter by default) - html += ` -
${htmlTab}
-
-
- ` + html += `
` + if (app.hasPrivilege('create_events')) { html += ` - + ` } @@ -956,13 +1071,24 @@ Class.subclass(Page.Base, "Page.Schedule", { update_job_last_runs: function () { // update last run state for all jobs, called when state is updated if (!app.state.jobCodes) return; + let isGrid = app.getPref('event_view') === 'grid' || app.getPref('event_view') == 'gridall' for (var event_id in app.state.jobCodes) { - var last_code = app.state.jobCodes[event_id]; - var status_html = last_code ? ' Error' : ' Success'; - if (last_code == 255) status_html = ' Warning' - // this.div.find('#ss_' + event_id).html(status_html); - document.getElementById('ss_' + event_id).innerHTML = status_html + let last_code = app.state.jobCodes[event_id]; + let status_html; + + if (isGrid) { + status_html = last_code ? 'event-error' : 'event-success' + if (last_code == 255) status_html = 'event-warning' + status_html = `` + } + else { + status_html = last_code ? ' Error' : ' Success'; + if (last_code == 255) status_html = ' Warning' + } + + let statusIcon = document.getElementById('ss_' + event_id) + if (statusIcon) statusIcon.innerHTML = status_html } }, @@ -990,6 +1116,14 @@ Class.subclass(Page.Base, "Page.Schedule", { this.gosub_events(this.args); }, + change_event_view: function (view_type) { + // if( ['Grid', 'Details', 'Grid-All'].indexOf(view_type) < 0 ) view_type = 'Details' + if (['details', 'grid', 'gridall'].indexOf(view_type) < 0) view_type = 'details' + app.setPref('event_view', view_type) + this.gosub_events(this.args); + + }, + toggle_group_by: function () { let args = this.args args.collapse ^= true @@ -1524,7 +1658,7 @@ Class.subclass(Page.Base, "Page.Schedule", { } // check for update delete this.old_event; - if (event.secret_value && typeof event.secret_value === 'string' ) { + if (event.secret_value && typeof event.secret_value === 'string') { delete event.secret_value $('#fe_ee_secret').val('').attr('placeholder', '[*****]') } @@ -1734,9 +1868,9 @@ Class.subclass(Page.Base, "Page.Schedule", { } // Secret - let sph = event.secret_preview ? '[' + event.secret_preview + ']' : ''; + let sph = event.secret_preview ? '[' + event.secret_preview + ']' : ''; html += get_form_table_row('Secret', ``); - html += get_form_table_caption("Specify KEY=VALUE pairs to be mounted as env variables (to this job process)"); + html += get_form_table_caption("Specify KEY=VALUE pairs to be mounted as env variables (to this job process). When using Docker plugin, KEY should be prefixed with DOCKER_ to pass custom variable to docker container. When using SSH plugin, KEY should be prefixed with SSH_ to pass to remote machine."); html += get_form_table_spacer(); // max children @@ -2537,13 +2671,13 @@ Class.subclass(Page.Base, "Page.Schedule", { this.refresh_plugin_params(); }, - - setScriptEditor: function() { - + + setScriptEditor: function () { + let params = this.event.params || {} let el = document.getElementById("fe_ee_pp_script") - if(!el) return + if (!el) return let privs = app.user.privileges; let canEdit = privs.admin || privs.edit_events || privs.create_events; @@ -2585,45 +2719,45 @@ Class.subclass(Page.Base, "Page.Schedule", { gutters: [gutter], lint: lint, extraKeys: { - "F11": (cm) => cm.setOption("fullScreen", !cm.getOption("fullScreen")), - "Esc": (cm) => cm.getOption("fullScreen") ? cm.setOption("fullScreen", false) : null, - "Ctrl-/": (cm) => cm.execCommand('toggleComment') - } - }); + "F11": (cm) => cm.setOption("fullScreen", !cm.getOption("fullScreen")), + "Esc": (cm) => cm.getOption("fullScreen") ? cm.setOption("fullScreen", false) : null, + "Ctrl-/": (cm) => cm.execCommand('toggleComment') + } + }); editor.on('change', (cm) => { el.value = cm.getValue() }) // syntax selector - document.getElementById("fe_ee_pp_lang").addEventListener("change", function(){ + document.getElementById("fe_ee_pp_lang").addEventListener("change", function () { let ln = this.options[this.selectedIndex].value; editor.setOption("gutters", ['']); editor.setOption("lint", false) - if(ln == 'java') {ln = 'text/x-java'} - if(ln == 'scala') {ln = 'text/x-scala'} - if(ln == 'csharp') {ln = 'text/x-csharp'} + if (ln == 'java') { ln = 'text/x-java' } + if (ln == 'scala') { ln = 'text/x-scala' } + if (ln == 'csharp') { ln = 'text/x-csharp' } if (ln == 'sql') { ln = 'text/x-sql' } if (ln == 'dockerfile') { ln = 'text/x-dockerfile' } if (ln == 'toml') { ln = 'text/x-toml' } - if (ln == 'json') { + if (ln == 'json') { ln = 'application/json' editor.setOption("lint", CodeMirror.lint.json) - } + } if (ln == 'props') { ln = 'text/x-properties' } if (ln == 'yaml') { ln = 'text/x-yaml' editor.setOption("gutters", ['CodeMirror-lint-markers']); editor.setOption("lint", CodeMirror.lint.yaml) } - editor.setOption("mode", ln); + editor.setOption("mode", ln); }); // theme - document.getElementById("fe_ee_pp_theme").addEventListener("change", function(){ + document.getElementById("fe_ee_pp_theme").addEventListener("change", function () { var thm = this.options[this.selectedIndex].value; - if(thm === 'default' && app.getPref('theme') === 'dark') thm = 'gruvbox-dark'; - if(thm === 'light') thm = 'default'; + if (thm === 'default' && app.getPref('theme') === 'dark') thm = 'gruvbox-dark'; + if (thm === 'light') thm = 'default'; editor.setOption("theme", thm); }); }, @@ -2725,7 +2859,7 @@ Class.subclass(Page.Base, "Page.Schedule", { refresh_plugin_params: function () { // redraw plugin param area after change $('#d_ee_plugin_params').html(this.get_plugin_params_html()); - this.setScriptEditor(); + this.setScriptEditor(); }, get_random_event: function () { diff --git a/package.json b/package.json index 7d27132..1127cb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cronicle-edge", - "version": "1.8.2", + "version": "1.9.0", "description": "A simple, distributed task scheduler and runner with a web based UI.", "author": "Joseph Huckaby ", "homepage": "https://github.com/jhuckaby/Cronicle", diff --git a/sample_conf/setup.json b/sample_conf/setup.json index e286d10..e90d530 100644 --- a/sample_conf/setup.json +++ b/sample_conf/setup.json @@ -87,9 +87,10 @@ { "id":"ssh_host", "type":"text", "size":40, "title":"SSH Host", "value": "ssh://cronicle:Password@localhost:22" }, { "id":"kill_cmd", "type":"text", "size":40, "title":"Kill Command", "value": "pkill -s $$" }, { "id":"script", "type":"textarea", "rows":10, "title":"Script", "value": "\n#!/bin/sh\n\necho \"Running SSHX job on $HOSTNAME\"\n\nsleep 10\n\n# Specify conneciton info using URI format:\n# [ssh://]user[:Password]@host[:port]?[privateKey=/path/to/file&passphrase=Password]\n# URI can be passed directly or as a reference to environment variable\n# If passing password via URI, you'd need to encode special characters (e.g. @ => %40)\n# You can also set SSH_KEY/SSH_PASSWORD/SSH_PASSPHRASE as variables in event/plugin secrets\n\n# If no SSH Host specified on event parameters, it can be resolved from WF argument\n\n# Kill Command parameter is used to properly handle job abortion\n# Default command is [pkill -S $$]\n# $$ is refering to PID of your script parent process (bootstrap script)\n\n# To pass variables other then JOB_ / ARG*, use SSH_ prefix on variable name\n# If this event is chained by other job, you can access chain data using CHAIN_DATA variable\n\n" }, - {"type":"select","id":"lang","title":"syntax","items":["shell","powershell","javascript","python","perl","groovy","java","csharp","scala","sql","yaml","dockerfile","json","props"],"value":"shell"}, - {"type":"select","id":"theme","title":"theme","items":["default","gruvbox-dark","solarized light","solarized dark","darcula","ambiance","base16-dark","nord"],"value":"default"}, - { "id":"json", "type":"checkbox", "title":"Interpret JSON in Output", "value": 0 } + { "id":"json", "type":"checkbox", "title":"Interpret JSON in Output", "value": 0 }, + { "type": "checkbox", "id": "annotate", "title": "Add Date/Time Stamps to Log", "value": 0 }, + { "type":"select","id":"lang","title":"syntax","items":["shell","powershell","javascript","python","perl","groovy","java","csharp","scala","sql","yaml","dockerfile","json","props"],"value":"shell"}, + { "type":"select","id":"theme","title":"theme","items":["default","gruvbox-dark","solarized light","solarized dark","darcula","ambiance","base16-dark","nord"],"value":"default"} ] } ], @@ -109,6 +110,7 @@ { "id":"command", "type":"text", "size":40, "title":"Command", "value": "" }, { "id":"script", "type":"textarea", "rows":10, "title":"Script", "value": "\n#!/bin/sh\n\necho \"Running Docker job on $HOSTNAME\"\n\ncat /etc/os-release\n\nls -lah chain_data || echo \"no chain data\"\n\n# If Docker Host is not specified, docker socket will be used\n# To access remote docker machine via ssh (on top of socket) specify DH as:\n# ssh://user[:password]@host[:port]\n# To specify ssh credentials you can also use SSH_PASSWORD/SSH_KEY env varaibles\n# To access remote docker instance with exposed http:\n# http://dockerhost:2375 (there is no auth options for that at this point)\n# To specify image registry credentials use DOCKER_USER, DOCKER_PASSWORD variables\n# All credential variables can be set via event/plugin secrets\n\n# This script will be used as an entrypoint on the container (mounted as /cronicle.sh by default).\n# To use original entrypoint check corresponding checkbox below,\n# and use command parameter above to pass argument to it\n# If this job is chained by other event, chain data will be mounted to the container\n# as chain_data file \n\n# In order to pass variables to container (other than JOB_ and ARG*) it should start with DOCKER_\n\n# If job is aborted \"docker stop\" is invoked (SIGTERM), docker will send SIGKILL after some time\n# Try to handle SIGTERM in your script for proper shutdown on abortion\n"}, { "id":"entrypoint_path", "type":"text", "size":40, "title":"Mount As", "value": "" }, + { "id":"network", "type":"text", "size":20, "title":"Network", "value": "" }, { "id":"keep_entrypoint", "type":"checkbox", "title":"Keep Original Entrypoint", "value": 0 }, { "id":"pull_image", "type":"checkbox", "title":"Pull Image", "value": 1 }, { "id":"keep_container", "type":"checkbox", "title":"Keep Container", "value": 0 },
  Add Event...
 
  Random
  Generate