Phase 9: rebuild frontend on a shared layout + fix Monitor Jobs race
The nav link race:
- Every page's header used `<a id="jobLink" href=''>` and the real
destination was only assigned later, after /info returned. An empty
href resolves to the current document URL, so clicking "Monitor Jobs"
on sendMessage before /info completed silently reloaded sendMessage.
- Fixed structurally: nav links now use static relative hrefs baked
into the HTML ("./sendMessage.html", "./monitorJobs.html"), so the
destination is correct the moment the DOM parses.
Shared UI:
- New html/css/app.css: design tokens (palette, radius, shadow, font),
sticky compact top header (bot avatar + label on the left, nav pills
in the middle, current user on the right, active-page highlight via
aria-current), card containers, form styling with focus rings,
DataTables theme overrides, status pills, modal, and responsive
breakpoints.
- New html/js/app.js: shared browser bootstrap. Parses appName from
the URL, redirects to OAuth if the id cookie is missing, fetches
/info once, populates the header, applies aria-current to the
active nav link, and invokes a per-page onReady callback with
{ info, appName }. Also exports getCookie, escapeHtml, and formatDate
helpers so each page stops shipping its own copy.
Per-page rewrites:
- sendMessage.html/.js: form now lives in a card, image preview only
shows when a file is attached, EasyMDE + VirtualSelect styled to
match the theme, submit is a primary button, confirmation modal
redesigned. All bootstrap code deleted (delegated to app.js).
- monitorJobs.html/.js: three cards (Running / Scheduled / Completed)
with themed DataTables. Completed table sorts by start time desc,
paginates, and searches; message column truncates HTML previews to
~120 chars. Empty-state text per table. `jobId` and running-row
"view" links go to jobDetail via safe relative URLs.
- jobDetail.html/.js: same shared header + card layout; summary grid,
message preview, and recipient table styled to match the new
palette.
Sanity checks:
- All 43 helper tests still pass.
- Server boots cleanly on port 3001.
- Curl of sendMessage/monitorJobs/jobDetail all return 200 with the
shared header markup.
- /CollabCentral/:app/css/app.css and /CollabCentral/:app/js/app.js
both serve 200 (shared static mount is per-bot as expected).
- No local href in any page is empty; every nav target resolves at
parse time.
- /info and requireBot 404 gate unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2b37c4b24f
commit
2984e8e851
9 changed files with 1103 additions and 882 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"server": {
|
"server": {
|
||||||
"name": "CollabCentral",
|
"name": "CollabCentral",
|
||||||
"port": "1450"
|
"port": "1451"
|
||||||
},
|
},
|
||||||
"languages": [],
|
"languages": [],
|
||||||
"webex": {
|
"webex": {
|
||||||
|
|
|
||||||
514
html/css/app.css
Normal file
514
html/css/app.css
Normal file
|
|
@ -0,0 +1,514 @@
|
||||||
|
/* ============================================================================
|
||||||
|
CollabCentral shared UI. All three top-level pages (sendMessage, monitorJobs,
|
||||||
|
jobDetail) render on top of this stylesheet — every rule below is meant to
|
||||||
|
be reusable across pages, and per-page tweaks stay in a small `<style>` at
|
||||||
|
the top of each HTML file.
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card: #ffffff;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--border-strong: #cbd5e1;
|
||||||
|
--text: #0f172a;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--text-subtle: #94a3b8;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-hover: #1d4ed8;
|
||||||
|
--accent-soft: #dbeafe;
|
||||||
|
--success: #15803d;
|
||||||
|
--success-bg: #dcfce7;
|
||||||
|
--danger: #b91c1c;
|
||||||
|
--danger-bg: #fee2e2;
|
||||||
|
--warning: #a16207;
|
||||||
|
--warning-bg: #fef9c3;
|
||||||
|
--radius: 8px;
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||||
|
--shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
|
||||||
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--content-max: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover { color: var(--accent-hover); text-decoration: underline; }
|
||||||
|
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* ------- Top nav ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.appHeader {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--card);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__inner {
|
||||||
|
max-width: var(--content-max);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.6em 1.25em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7em;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__nav a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.4em 0.85em;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.appHeader__nav a:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.appHeader__nav a[aria-current="page"] {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.appHeader__spacer { flex: 1; }
|
||||||
|
|
||||||
|
.appHeader__user {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 20ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Page container / cards ------------------------------------------ */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: var(--content-max);
|
||||||
|
margin: 1.5em auto;
|
||||||
|
padding: 0 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
padding: 1.5em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 { margin-top: 0; }
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
margin: 0 0 0.2em;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSubtitle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0 0 1.25em;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Panels (empty state) -------------------------------------------- */
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 4em auto;
|
||||||
|
padding: 2.5em 2em;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.panel h2 {
|
||||||
|
margin: 0 0 0.4em;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.panel p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Forms ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field > label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.35em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input[type="text"],
|
||||||
|
.field input[type="datetime-local"],
|
||||||
|
.field input[type="file"],
|
||||||
|
.field textarea,
|
||||||
|
.field select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.55em 0.75em;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font: inherit;
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input[type="text"]:focus,
|
||||||
|
.field input[type="datetime-local"]:focus,
|
||||||
|
.field textarea:focus,
|
||||||
|
.field select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field .hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File input tweak: browsers style these very differently; give it some room */
|
||||||
|
.field input[type="file"] {
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EasyMDE overrides so it fits our neutral theme */
|
||||||
|
.EasyMDEContainer .CodeMirror {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--card);
|
||||||
|
}
|
||||||
|
.EasyMDEContainer .editor-toolbar {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background: var(--bg);
|
||||||
|
border-top-left-radius: var(--radius-sm);
|
||||||
|
border-top-right-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.EasyMDEContainer .editor-preview,
|
||||||
|
.EasyMDEContainer .editor-preview-side {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image preview */
|
||||||
|
#file-ip-1-preview {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 260px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-top: 0.4em;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
#file-ip-1-preview:not([src]) { display: none; }
|
||||||
|
|
||||||
|
/* ------- Buttons --------------------------------------------------------- */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.55em 1.1em;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--bg); }
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
border-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: transparent;
|
||||||
|
padding: 0.35em 0.7em;
|
||||||
|
}
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6em;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Tables (DataTables-friendly) ------------------------------------ */
|
||||||
|
|
||||||
|
table.dataTable {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
table.dataTable thead th {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border-bottom: 1px solid var(--border) !important;
|
||||||
|
padding: 0.7em 0.8em !important;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
table.dataTable tbody td {
|
||||||
|
padding: 0.7em 0.8em !important;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
table.dataTable tbody tr:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
table.dataTable tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
table.dataTable.no-footer { border-bottom: none; }
|
||||||
|
|
||||||
|
.dataTables_wrapper .dataTables_filter input {
|
||||||
|
padding: 0.35em 0.6em;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_wrapper .dataTables_length select {
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_wrapper .dataTables_info,
|
||||||
|
.dataTables_wrapper .dataTables_paginate {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding-top: 0.75em;
|
||||||
|
}
|
||||||
|
.dataTables_wrapper .paginate_button {
|
||||||
|
padding: 0.35em 0.7em !important;
|
||||||
|
margin-left: 0.15em;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
}
|
||||||
|
.dataTables_wrapper .paginate_button.current,
|
||||||
|
.dataTables_wrapper .paginate_button:hover {
|
||||||
|
background: var(--accent-soft) !important;
|
||||||
|
color: var(--accent) !important;
|
||||||
|
border-color: var(--accent-soft) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Job-detail summary grid ----------------------------------------- */
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 0.5em 1.5em;
|
||||||
|
padding: 1.1em 1.25em;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin: 0 0 1.5em;
|
||||||
|
}
|
||||||
|
.summary-grid dt {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
.summary-grid dd {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 1.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-preview {
|
||||||
|
padding: 1em 1.25em;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.message-preview img { max-width: 100%; height: auto; }
|
||||||
|
.message-preview p:first-child { margin-top: 0; }
|
||||||
|
.message-preview p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
/* ------- Pills ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.15em 0.7em;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.pill.ok { background: var(--success-bg); color: var(--success); }
|
||||||
|
.pill.err { background: var(--danger-bg); color: var(--danger); }
|
||||||
|
.pill.pending { background: var(--warning-bg); color: var(--warning); }
|
||||||
|
|
||||||
|
/* ------- Modal ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.4);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 21;
|
||||||
|
width: min(440px, 92vw);
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 1.5em 1.75em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6em;
|
||||||
|
}
|
||||||
|
.modal h3 { margin: 0; }
|
||||||
|
.modal p { color: var(--text-muted); margin: 0; }
|
||||||
|
.modal__close {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.modal__close:hover { background: var(--bg); color: var(--text); }
|
||||||
|
.modal__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6em;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- Responsive ------------------------------------------------------ */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.field-row { grid-template-columns: 1fr; }
|
||||||
|
.appHeader__inner { flex-wrap: wrap; }
|
||||||
|
.appHeader__nav { margin-left: 0; }
|
||||||
|
.container { padding: 0 1em; }
|
||||||
|
.card { padding: 1.1em; }
|
||||||
|
}
|
||||||
|
|
@ -1,144 +1,48 @@
|
||||||
<html>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>CollabCentral</title>
|
<title>CollabCentral</title>
|
||||||
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
<link id="appFavicon" rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<style>
|
<link rel="stylesheet" href="css/app.css">
|
||||||
* { box-sizing: border-box; }
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css">
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
color: #2F3941;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: block;
|
|
||||||
background-color: tan;
|
|
||||||
width: 90%;
|
|
||||||
height: auto;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#appInfo {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: block;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 1em auto;
|
|
||||||
padding: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2.jobTitle {
|
|
||||||
margin: 0.5em 0 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: #6c757d;
|
|
||||||
margin: 0 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: max-content 1fr;
|
|
||||||
gap: 0.45em 1.5em;
|
|
||||||
margin: 1em 0 1.5em;
|
|
||||||
padding: 1em 1.2em;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-grid dt {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-grid dd { margin: 0; }
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 1.05em;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 1.5em 0 0.4em;
|
|
||||||
color: #2F3941;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-preview {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 1em 1.2em;
|
|
||||||
background: #fafafa;
|
|
||||||
margin: 0.4em 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-preview img { max-width: 100%; height: auto; }
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.15em 0.7em;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill.ok { background: #d4edda; color: #155724; }
|
|
||||||
.pill.err { background: #f8d7da; color: #721c24; }
|
|
||||||
.pill.pending { background: #fff3cd; color: #856404; }
|
|
||||||
|
|
||||||
td { white-space: pre-wrap !important; word-wrap: break-word; }
|
|
||||||
|
|
||||||
.hidden { display: none; }
|
|
||||||
|
|
||||||
.panel-empty {
|
|
||||||
padding: 2em;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 2em auto;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css" />
|
|
||||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body data-page="monitor" data-title-suffix="Job Detail">
|
||||||
<div class="header">
|
|
||||||
<div id="appInfo">
|
|
||||||
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
|
||||||
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center;"><a id="jobLink" href=''>Back to Jobs</a></div>
|
|
||||||
<div id="name" style="text-align: right;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="unauthorizedPanel" class="panel-empty hidden">
|
<header class="appHeader">
|
||||||
|
<div class="appHeader__inner">
|
||||||
|
<div class="appHeader__brand">
|
||||||
|
<div id="appAvatar" class="appHeader__avatar"></div>
|
||||||
|
<div id="appLabel" class="appHeader__label">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<nav class="appHeader__nav">
|
||||||
|
<a href="./sendMessage.html" data-nav="send">Send Message</a>
|
||||||
|
<a href="./monitorJobs.html" data-nav="monitor">Monitor Jobs</a>
|
||||||
|
</nav>
|
||||||
|
<div class="appHeader__spacer"></div>
|
||||||
|
<div id="appUser" class="appHeader__user"></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="unauthorizedPanel" class="panel hidden">
|
||||||
<h2>You don't have access to this bot</h2>
|
<h2>You don't have access to this bot</h2>
|
||||||
<p>Your account isn't on the authorized list. Contact the bot's administrator.</p>
|
<p>Your account isn't on the authorized list for this bot. Contact the bot's administrator.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="notFoundPanel" class="panel-empty hidden">
|
<div id="notFoundPanel" class="panel hidden">
|
||||||
<h2>Job not found</h2>
|
<h2>Job not found</h2>
|
||||||
<p>The job you're looking for doesn't exist, has been purged, or belongs to a different bot.</p>
|
<p>The job you're looking for doesn't exist, has been purged, or belongs to a different bot.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="detailPanel" class="container hidden">
|
<div id="detailPanel" class="container hidden">
|
||||||
<h2 class="jobTitle" id="jobTitle"></h2>
|
<div class="card">
|
||||||
<p class="subtitle" id="subtitle"></p>
|
<h1 class="pageTitle" id="jobTitle"></h1>
|
||||||
|
<p class="pageSubtitle" id="subtitle"></p>
|
||||||
|
|
||||||
<dl class="summary-grid" id="summaryGrid"></dl>
|
<dl class="summary-grid" id="summaryGrid"></dl>
|
||||||
|
|
||||||
|
|
@ -146,19 +50,21 @@
|
||||||
<div class="message-preview" id="messagePreview"></div>
|
<div class="message-preview" id="messagePreview"></div>
|
||||||
|
|
||||||
<div class="section-title">Recipients (<span id="recipientCount">0</span>)</div>
|
<div class="section-title">Recipients (<span id="recipientCount">0</span>)</div>
|
||||||
<table id="recipients" style="width: 100%;">
|
<table id="recipients" class="dataTable" style="width: 100%;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Send Time</th>
|
<th>Send time</th>
|
||||||
<th>Error</th>
|
<th>Error</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/app.js"></script>
|
||||||
<script src="jobDetail.js"></script>
|
<script src="jobDetail.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,74 @@
|
||||||
var pathArray = window.location.pathname.split('/');
|
// Page script for jobDetail.html. Shared bootstrapping lives in js/app.js;
|
||||||
var appName = pathArray[2];
|
// this file just fetches the requested job and renders the detail view.
|
||||||
|
|
||||||
var urlParams = new URLSearchParams(window.location.search);
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
var jobId = urlParams.get('jobId');
|
var jobId = urlParams.get('jobId');
|
||||||
|
|
||||||
if (!getCookie("id")) {
|
CollabCentral.init(function (ctx) {
|
||||||
var randomNumber = Math.random().toString().substring(2);
|
loadJobDetail(ctx.appName);
|
||||||
fetch('/CollabCentral/' + appName + '/authUrl')
|
});
|
||||||
.then(res => res.text())
|
|
||||||
.then(res => { window.location.href = res + randomNumber; });
|
|
||||||
} else {
|
|
||||||
bootstrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
function bootstrap() {
|
function loadJobDetail(appName) {
|
||||||
fetch('/CollabCentral/' + appName + '/info')
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(info => {
|
|
||||||
document.title = info.label + " — Job #" + (jobId || "?");
|
|
||||||
document.getElementById("appLabel").innerHTML = info.label;
|
|
||||||
document.getElementById("name").innerHTML = getCookie("displayName") || "";
|
|
||||||
document.getElementById("appName").href = info.faviconUrl;
|
|
||||||
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/monitorJobs.html";
|
|
||||||
|
|
||||||
var appIcon = document.getElementById("appIcon");
|
|
||||||
appIcon.src = info.iconUrl;
|
|
||||||
appIcon.style.maxHeight = "150px";
|
|
||||||
appIcon.style.maxWidth = "150px";
|
|
||||||
|
|
||||||
if (!info.authorized) {
|
|
||||||
document.getElementById("unauthorizedPanel").classList.remove("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadJobDetail();
|
|
||||||
})
|
|
||||||
.catch(err => console.error("Failed to load /info:", err));
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadJobDetail() {
|
|
||||||
if (!jobId) {
|
if (!jobId) {
|
||||||
document.getElementById("notFoundPanel").classList.remove("hidden");
|
document.getElementById('notFoundPanel').classList.remove('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch('/CollabCentral/' + appName + '/jobs/detail/' + encodeURIComponent(jobId))
|
fetch('/CollabCentral/' + appName + '/jobs/detail/' + encodeURIComponent(jobId))
|
||||||
.then(res => {
|
.then(function (res) {
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
document.getElementById("notFoundPanel").classList.remove("hidden");
|
document.getElementById('notFoundPanel').classList.remove('hidden');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(job => {
|
.then(function (job) {
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
renderJob(job);
|
renderJob(job);
|
||||||
document.getElementById("detailPanel").classList.remove("hidden");
|
document.getElementById('detailPanel').classList.remove('hidden');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(function (err) {
|
||||||
console.error("Failed to load job detail:", err);
|
console.error('Failed to load job detail:', err);
|
||||||
document.getElementById("notFoundPanel").classList.remove("hidden");
|
document.getElementById('notFoundPanel').classList.remove('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderJob(job) {
|
function renderJob(job) {
|
||||||
document.getElementById("jobTitle").textContent = "Job #" + (job.jobId || "?");
|
document.getElementById('jobTitle').textContent = 'Job #' + (job.jobId || '?');
|
||||||
var subtitle = "Sent by " + (job.senderDisplayName || "unknown");
|
|
||||||
if (job.startTime) subtitle += " · started " + formatDate(job.startTime);
|
var subtitle = 'Sent by ' + (job.senderDisplayName || 'unknown');
|
||||||
document.getElementById("subtitle").textContent = subtitle;
|
if (job.startTime) subtitle += ' · started ' + CollabCentral.formatDate(job.startTime);
|
||||||
|
document.getElementById('subtitle').textContent = subtitle;
|
||||||
|
|
||||||
var dl = document.getElementById("summaryGrid");
|
|
||||||
dl.innerHTML = "";
|
|
||||||
var stats = job.stats || {};
|
var stats = job.stats || {};
|
||||||
var rows = [
|
var rows = [
|
||||||
["Job ID", job.jobId],
|
['Job ID', job.jobId],
|
||||||
["Submitted by", job.senderDisplayName],
|
['Submitted by', job.senderDisplayName],
|
||||||
["Scheduled for", job.scheduledFor ? formatDate(job.scheduledFor) : "Sent immediately"],
|
['Scheduled for', job.scheduledFor ? CollabCentral.formatDate(job.scheduledFor) : 'Sent immediately'],
|
||||||
["Started", formatDate(job.startTime)],
|
['Started', CollabCentral.formatDate(job.startTime)],
|
||||||
["Completed", job.endTime ? formatDate(job.endTime) : "In progress"],
|
['Completed', job.endTime ? CollabCentral.formatDate(job.endTime) : 'In progress'],
|
||||||
["Total duration", stats.totalTime],
|
['Total duration', stats.totalTime],
|
||||||
["Webex time", stats.webexTime],
|
['Webex time', stats.webexTime],
|
||||||
["Average per message", stats.averageTime],
|
['Average per message', stats.averageTime],
|
||||||
["Recipients", (job.memberList || []).length],
|
['Recipients', (job.memberList || []).length],
|
||||||
["Delivered", formatSuccess(stats)]
|
['Delivered', formatSuccess(stats)],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
var dl = document.getElementById('summaryGrid');
|
||||||
|
dl.innerHTML = '';
|
||||||
for (var i = 0; i < rows.length; i++) {
|
for (var i = 0; i < rows.length; i++) {
|
||||||
var key = rows[i][0], val = rows[i][1];
|
var key = rows[i][0], val = rows[i][1];
|
||||||
if (val === undefined || val === null || val === "") continue;
|
if (val === undefined || val === null || val === '') continue;
|
||||||
var dt = document.createElement("dt"); dt.textContent = key;
|
var dt = document.createElement('dt');
|
||||||
var dd = document.createElement("dd"); dd.textContent = val;
|
dt.textContent = key;
|
||||||
dl.appendChild(dt); dl.appendChild(dd);
|
var dd = document.createElement('dd');
|
||||||
|
dd.textContent = val;
|
||||||
|
dl.appendChild(dt);
|
||||||
|
dl.appendChild(dd);
|
||||||
}
|
}
|
||||||
|
|
||||||
var preview = document.getElementById("messagePreview");
|
// Message preview: prefer HTML, fall back to markdown/text.
|
||||||
|
var preview = document.getElementById('messagePreview');
|
||||||
var msg = (job.message && job.message.english) || job.message || {};
|
var msg = (job.message && job.message.english) || job.message || {};
|
||||||
if (msg.html) {
|
if (msg.html) {
|
||||||
preview.innerHTML = msg.html;
|
preview.innerHTML = msg.html;
|
||||||
|
|
@ -99,14 +76,14 @@ function renderJob(job) {
|
||||||
preview.textContent = msg.markdown;
|
preview.textContent = msg.markdown;
|
||||||
} else if (msg.text) {
|
} else if (msg.text) {
|
||||||
preview.textContent = msg.text;
|
preview.textContent = msg.text;
|
||||||
} else if (typeof msg.raw === "string") {
|
} else if (typeof msg.raw === 'string') {
|
||||||
preview.textContent = msg.raw;
|
preview.textContent = msg.raw;
|
||||||
} else {
|
} else {
|
||||||
preview.innerHTML = "<em>No message content available.</em>";
|
preview.innerHTML = '<em>No message content available.</em>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var members = job.memberList || [];
|
var members = job.memberList || [];
|
||||||
document.getElementById("recipientCount").textContent = members.length;
|
document.getElementById('recipientCount').textContent = members.length;
|
||||||
|
|
||||||
$('#recipients').dataTable({
|
$('#recipients').dataTable({
|
||||||
data: members.map(formatRecipientRow),
|
data: members.map(formatRecipientRow),
|
||||||
|
|
@ -115,22 +92,22 @@ function renderJob(job) {
|
||||||
{ data: 'email' },
|
{ data: 'email' },
|
||||||
{ data: 'status' },
|
{ data: 'status' },
|
||||||
{ data: 'time' },
|
{ data: 'time' },
|
||||||
{ data: 'error' }
|
{ data: 'error' },
|
||||||
],
|
],
|
||||||
pageLength: 50,
|
pageLength: 50,
|
||||||
lengthMenu: [25, 50, 100, 250],
|
lengthMenu: [25, 50, 100, 250],
|
||||||
order: [[2, 'asc']],
|
order: [[2, 'asc']],
|
||||||
searching: true,
|
searching: true,
|
||||||
info: true,
|
info: true,
|
||||||
paging: true
|
paging: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRecipientRow(m) {
|
function formatRecipientRow(m) {
|
||||||
var result = m.results || null;
|
var result = m.results || null;
|
||||||
var status;
|
var status;
|
||||||
var email = "—";
|
var email = '—';
|
||||||
var err = "";
|
var err = '';
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
status = '<span class="pill pending">Pending</span>';
|
status = '<span class="pill pending">Pending</span>';
|
||||||
|
|
@ -142,38 +119,25 @@ function formatRecipientRow(m) {
|
||||||
email = result.toPersonEmail || email;
|
email = result.toPersonEmail || email;
|
||||||
if (result.message) err = result.message;
|
if (result.message) err = result.message;
|
||||||
else if (result.errors && result.errors.length) {
|
else if (result.errors && result.errors.length) {
|
||||||
err = result.errors.map(function (e) { return e.description || e.code || JSON.stringify(e); }).join("; ");
|
err = result.errors.map(function (e) {
|
||||||
|
return e.description || e.code || JSON.stringify(e);
|
||||||
|
}).join('; ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
displayName: m.displayName || "—",
|
displayName: m.displayName || '—',
|
||||||
email: email,
|
email: email,
|
||||||
status: status,
|
status: status,
|
||||||
time: m.msgTime ? (m.msgTime / 1000).toFixed(2) + " s" : "—",
|
time: m.msgTime ? (m.msgTime / 1000).toFixed(2) + ' s' : '—',
|
||||||
error: err
|
error: err,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSuccess(stats) {
|
function formatSuccess(stats) {
|
||||||
if (!stats || stats.succeededMsgs === undefined) return null;
|
if (!stats || stats.succeededMsgs === undefined) return null;
|
||||||
var pct = (typeof stats.succeededMsgPct === "number") ? stats.succeededMsgPct.toFixed(1) : stats.succeededMsgPct;
|
var pct = (typeof stats.succeededMsgPct === 'number')
|
||||||
return stats.succeededMsgs + " / " + stats.totalMsgs + " (" + pct + "%)";
|
? stats.succeededMsgPct.toFixed(1)
|
||||||
}
|
: stats.succeededMsgPct;
|
||||||
|
return stats.succeededMsgs + ' / ' + stats.totalMsgs + ' (' + pct + '%)';
|
||||||
function formatDate(d) {
|
|
||||||
if (!d) return null;
|
|
||||||
try { return new Date(d).toLocaleString(); } catch (e) { return d; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') c = c.substring(1);
|
|
||||||
if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
html/js/app.js
Normal file
159
html/js/app.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
// Shared browser bootstrap for the three CollabCentral pages (sendMessage,
|
||||||
|
// monitorJobs, jobDetail). Handles the auth cookie check, redirects to the
|
||||||
|
// OAuth flow when needed, fetches /info once, and populates the shared header
|
||||||
|
// (bot avatar, bot label, current user name, active-page highlight).
|
||||||
|
//
|
||||||
|
// Pages should:
|
||||||
|
// 1. Include /CollabCentral/<app>/js/app.js *before* their page script.
|
||||||
|
// 2. Set `<body data-page="send|monitor|detail">` so the header highlights
|
||||||
|
// the correct nav link.
|
||||||
|
// 3. Call `CollabCentral.init(onReady)` after DOMContentLoaded (or via a
|
||||||
|
// <script defer> at the bottom of <body>). `onReady({ info, appName })`
|
||||||
|
// fires once the user is authenticated AND authorized; otherwise the
|
||||||
|
// shared unauthorized panel is shown and the callback is skipped.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var pathArray = window.location.pathname.split('/');
|
||||||
|
// Path shape: /CollabCentral/<appName>/<file>
|
||||||
|
var appName = pathArray[2];
|
||||||
|
|
||||||
|
// ---- helpers exposed on window.CollabCentral ---------------------------
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
var target = name + '=';
|
||||||
|
var parts = decodeURIComponent(document.cookie || '').split(';');
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var c = parts[i].trim();
|
||||||
|
if (c.indexOf(target) === 0) return c.substring(target.length);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
if (s === null || s === undefined) return '';
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(input) {
|
||||||
|
if (!input) return '';
|
||||||
|
try {
|
||||||
|
var d = new Date(input);
|
||||||
|
if (isNaN(d.getTime())) return String(input);
|
||||||
|
return d.toLocaleString();
|
||||||
|
} catch (e) {
|
||||||
|
return String(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- header rendering --------------------------------------------------
|
||||||
|
|
||||||
|
function highlightActiveNav() {
|
||||||
|
var page = (document.body && document.body.getAttribute('data-page')) || '';
|
||||||
|
if (!page) return;
|
||||||
|
var links = document.querySelectorAll('.appHeader__nav a[data-nav]');
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
if (links[i].getAttribute('data-nav') === page) {
|
||||||
|
links[i].setAttribute('aria-current', 'page');
|
||||||
|
} else {
|
||||||
|
links[i].removeAttribute('aria-current');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeader(info) {
|
||||||
|
// Bot label + document title
|
||||||
|
if (info.label) {
|
||||||
|
var labelEl = document.getElementById('appLabel');
|
||||||
|
if (labelEl) labelEl.textContent = info.label;
|
||||||
|
var titleSuffix = (document.body && document.body.getAttribute('data-title-suffix')) || '';
|
||||||
|
document.title = titleSuffix ? info.label + ' — ' + titleSuffix : info.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot avatar
|
||||||
|
var avatarUrl = info.avatarUrl || info.iconUrl;
|
||||||
|
if (avatarUrl) {
|
||||||
|
var avatarEl = document.getElementById('appAvatar');
|
||||||
|
if (avatarEl) avatarEl.style.backgroundImage = 'url(' + JSON.stringify(avatarUrl) + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favicon
|
||||||
|
if (info.faviconUrl) {
|
||||||
|
var fav = document.getElementById('appFavicon');
|
||||||
|
if (fav) fav.href = info.faviconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User (from cookie set at OAuth completion)
|
||||||
|
var userEl = document.getElementById('appUser');
|
||||||
|
if (userEl) userEl.textContent = getCookie('displayName') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUnauthorized() {
|
||||||
|
// Every page includes an #unauthorizedPanel in its markup for this
|
||||||
|
// exact fallback. Reveal it and hide anything else the page might
|
||||||
|
// have already shown.
|
||||||
|
var el = document.getElementById('unauthorizedPanel');
|
||||||
|
if (el) el.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- redirect to OAuth when no session cookie is present ---------------
|
||||||
|
|
||||||
|
function redirectToOAuth() {
|
||||||
|
// Preserve the pre-existing behavior of appending a random query
|
||||||
|
// suffix so the browser does not cache the intermediate redirect.
|
||||||
|
var randomTail = Math.random().toString().substring(2);
|
||||||
|
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||||
|
.then(function (res) { return res.text(); })
|
||||||
|
.then(function (url) {
|
||||||
|
if (!url) return;
|
||||||
|
window.location.href = url + randomTail;
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('Failed to fetch /authUrl:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- top-level init ----------------------------------------------------
|
||||||
|
|
||||||
|
function init(onReady) {
|
||||||
|
highlightActiveNav();
|
||||||
|
|
||||||
|
if (!getCookie('id')) {
|
||||||
|
redirectToOAuth();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/CollabCentral/' + appName + '/info')
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function (info) {
|
||||||
|
renderHeader(info);
|
||||||
|
if (!info.authorized) {
|
||||||
|
showUnauthorized();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof onReady === 'function') {
|
||||||
|
onReady({ info: info, appName: appName });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('Failed to load /info:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- expose ------------------------------------------------------------
|
||||||
|
|
||||||
|
window.CollabCentral = {
|
||||||
|
appName: appName,
|
||||||
|
init: init,
|
||||||
|
getCookie: getCookie,
|
||||||
|
escapeHtml: escapeHtml,
|
||||||
|
formatDate: formatDate,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
@ -1,94 +1,60 @@
|
||||||
<html>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>CollabCentral</title>
|
<title>CollabCentral</title>
|
||||||
<style>
|
<link id="appFavicon" rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
* {
|
<link rel="stylesheet" href="css/app.css">
|
||||||
box-sizing: border-box;
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css">
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
color: #2F3941;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#table {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
white-space: pre-wrap !important;
|
|
||||||
word-wrap:break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: block;
|
|
||||||
background-color: tan;
|
|
||||||
width: 80%;
|
|
||||||
height: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: block;
|
|
||||||
text-align: right;
|
|
||||||
align-content: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#appInfo {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
align-content: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css" />
|
|
||||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body data-page="monitor" data-title-suffix="Monitor Jobs">
|
||||||
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
|
||||||
|
|
||||||
<div class="header">
|
<header class="appHeader">
|
||||||
<div id="appInfo">
|
<div class="appHeader__inner">
|
||||||
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
<div class="appHeader__brand">
|
||||||
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
<div id="appAvatar" class="appHeader__avatar"></div>
|
||||||
|
<div id="appLabel" class="appHeader__label">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;"><a id="jobLink" href=''>Send Message</a></div>
|
<nav class="appHeader__nav">
|
||||||
<div id="name" style="text-align: right;"></div>
|
<a href="./sendMessage.html" data-nav="send">Send Message</a>
|
||||||
|
<a href="./monitorJobs.html" data-nav="monitor">Monitor Jobs</a>
|
||||||
|
</nav>
|
||||||
|
<div class="appHeader__spacer"></div>
|
||||||
|
<div id="appUser" class="appHeader__user"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div id="unauthorizedPanel" class="hidden" style="padding: 2em; max-width: 600px; margin: 2em auto; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
<div id="unauthorizedPanel" class="panel hidden">
|
||||||
<h2>You don't have access to this bot</h2>
|
<h2>You don't have access to this bot</h2>
|
||||||
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="jobsPanel" class="hidden">
|
<div id="jobsPanel" class="container hidden">
|
||||||
<h3>Running Jobs</h3>
|
<div class="card">
|
||||||
<div class="table"></div>
|
<h2 class="pageTitle" style="font-size: 1.05rem;">Running</h2>
|
||||||
<table id="runningJobs" class="table">
|
<p class="pageSubtitle">Jobs currently sending.</p>
|
||||||
|
<table id="runningJobs" class="dataTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Submitter</th>
|
<th>Submitter</th>
|
||||||
<th>App</th>
|
<th>App</th>
|
||||||
<th>Start Time</th>
|
<th>Start time</th>
|
||||||
<th>Total Recipients</th>
|
<th>Total recipients</th>
|
||||||
<th>Completed Recipients</th>
|
<th>Completed</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
<h3>Scheduled Jobs</h3>
|
</div>
|
||||||
<table id="scheduledJobs">
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="pageTitle" style="font-size: 1.05rem;">Scheduled</h2>
|
||||||
|
<p class="pageSubtitle">Jobs waiting to fire.</p>
|
||||||
|
<table id="scheduledJobs" class="dataTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Submitter</th>
|
<th>Submitter</th>
|
||||||
|
|
@ -99,27 +65,29 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Completed Jobs</h3>
|
<div class="card">
|
||||||
<table id="completedJobs">
|
<h2 class="pageTitle" style="font-size: 1.05rem;">Completed</h2>
|
||||||
|
<p class="pageSubtitle">Jobs retained for 30 days. Click a job ID to open its detail view.</p>
|
||||||
|
<table id="completedJobs" class="dataTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>jobId</th>
|
<th>Job</th>
|
||||||
<th>Submitter</th>
|
<th>Submitter</th>
|
||||||
<th>App</th>
|
<th>App</th>
|
||||||
<th>Message</th>
|
<th>Started</th>
|
||||||
<th>Start Time</th>
|
<th>Completed</th>
|
||||||
<th>End Time</th>
|
<th>Total time</th>
|
||||||
<th>Total Time</th>
|
<th>Delivered</th>
|
||||||
<th>Webex Time</th>
|
<th>Success rate</th>
|
||||||
<th>Average Message Time</th>
|
|
||||||
<th>Messages Delivered</th>
|
|
||||||
<th>Total Messages</th>
|
|
||||||
<th>Success Rate</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/app.js"></script>
|
||||||
<script src="monitorJobs.js"></script>
|
<script src="monitorJobs.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,126 @@
|
||||||
var pathArray = window.location.pathname.split('/');
|
// Page script for monitorJobs.html. Shared bootstrapping lives in js/app.js;
|
||||||
var appName = pathArray[2];
|
// this file just wires the three DataTables (running, scheduled, completed)
|
||||||
|
// once the user is confirmed authorized.
|
||||||
|
|
||||||
if (getCookie("id")) {
|
CollabCentral.init(function (ctx) {
|
||||||
console.log("Found ID Cookie: " + getCookie("id"));
|
document.getElementById('jobsPanel').classList.remove('hidden');
|
||||||
|
initJobTables(ctx.appName);
|
||||||
// Fetch bot metadata + authorization state, then render the page.
|
|
||||||
fetch('/CollabCentral/' + appName + '/info')
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(info => {
|
|
||||||
document.title = info.label;
|
|
||||||
document.getElementById("appLabel").innerHTML = info.label;
|
|
||||||
document.getElementById("name").innerHTML = getCookie("displayName") || "";
|
|
||||||
document.getElementById("appName").href = info.faviconUrl;
|
|
||||||
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/sendMessage.html";
|
|
||||||
|
|
||||||
var appIcon = document.getElementById("appIcon");
|
|
||||||
appIcon.src = info.iconUrl;
|
|
||||||
appIcon.style.maxHeight = "150px";
|
|
||||||
appIcon.style.maxWidth = "150px";
|
|
||||||
|
|
||||||
if (!info.authorized) {
|
|
||||||
document.getElementById("unauthorizedPanel").classList.remove("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("jobsPanel").classList.remove("hidden");
|
|
||||||
initJobTables();
|
|
||||||
})
|
|
||||||
.catch(err => console.error("Failed to load /info:", err));
|
|
||||||
} else {
|
|
||||||
console.log("No ID Cookie found.");
|
|
||||||
|
|
||||||
var randomNumber = Math.random().toString();
|
|
||||||
randomNumber = randomNumber.substring(2, randomNumber.length);
|
|
||||||
fetch('/CollabCentral/' + appName + '/authUrl')
|
|
||||||
.then(res => res.text())
|
|
||||||
.then((res) => {
|
|
||||||
window.location.href = res + randomNumber;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function jobDetailLink(appName, jobId, label) {
|
||||||
|
if (jobId === undefined || jobId === null) return CollabCentral.escapeHtml(label);
|
||||||
|
return '<a href="./jobDetail.html?jobId=' + encodeURIComponent(jobId) + '">' +
|
||||||
|
CollabCentral.escapeHtml(label) + '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function initJobTables() {
|
function renderDate(val) {
|
||||||
|
return val ? CollabCentral.escapeHtml(CollabCentral.formatDate(val)) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuccessRate(pct) {
|
||||||
|
if (pct === undefined || pct === null || pct === '') return '';
|
||||||
|
var n = typeof pct === 'number' ? pct : parseFloat(pct);
|
||||||
|
if (isNaN(n)) return CollabCentral.escapeHtml(pct);
|
||||||
|
return n.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDelivered(row) {
|
||||||
|
var s = row.stats || {};
|
||||||
|
if (s.succeededMsgs === undefined || s.totalMsgs === undefined) return '';
|
||||||
|
return s.succeededMsgs + ' / ' + s.totalMsgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initJobTables(appName) {
|
||||||
$('#runningJobs').dataTable({
|
$('#runningJobs').dataTable({
|
||||||
ajax: {
|
ajax: {
|
||||||
url: '/CollabCentral/' + appName + '/jobs/list/running',
|
url: '/CollabCentral/' + appName + '/jobs/list/running',
|
||||||
dataSrc: ''
|
dataSrc: '',
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{ data: 'senderDisplayName' },
|
{ data: 'senderDisplayName' },
|
||||||
{ data: 'appName' },
|
{ data: 'appName' },
|
||||||
{ data: 'startTime' },
|
{ data: 'startTime', render: function (v) { return renderDate(v); } },
|
||||||
{ data: 'totalRecipients' },
|
{ data: 'totalRecipients' },
|
||||||
{
|
{
|
||||||
data: 'completedRecipients',
|
data: 'completedRecipients',
|
||||||
render: function (val, type, row) {
|
render: function (val, type, row) {
|
||||||
if (type !== 'display' || row.jobId === undefined || row.jobId === null) return val;
|
if (type !== 'display' || row.jobId === undefined || row.jobId === null) return val;
|
||||||
return val + ' <a href="/CollabCentral/' + appName + '/jobDetail.html?jobId=' + encodeURIComponent(row.jobId) + '" style="margin-left: 0.5em; font-size: 0.85em;">view</a>';
|
var text = (val === undefined || val === null) ? 'view' : val + ' · view';
|
||||||
}
|
return jobDetailLink(appName, row.jobId, text);
|
||||||
}
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
searching: false,
|
searching: false,
|
||||||
info: false,
|
info: false,
|
||||||
ordering: false,
|
ordering: false,
|
||||||
paging: false
|
paging: false,
|
||||||
|
language: { emptyTable: 'No running jobs.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#scheduledJobs').dataTable({
|
$('#scheduledJobs').dataTable({
|
||||||
ajax: {
|
ajax: {
|
||||||
url: '/CollabCentral/' + appName + '/jobs/list/scheduled',
|
url: '/CollabCentral/' + appName + '/jobs/list/scheduled',
|
||||||
dataSrc: ''
|
dataSrc: '',
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{ data: 'senderDisplayName' },
|
{ data: 'senderDisplayName' },
|
||||||
{ data: 'appName' },
|
{ data: 'appName' },
|
||||||
{ data: 'message' },
|
{
|
||||||
|
data: 'message',
|
||||||
|
render: function (val, type, row) {
|
||||||
|
if (type !== 'display') return '';
|
||||||
|
// Message is stored per-language; prefer english.html or english.markdown.
|
||||||
|
var m = (val && (val.english || val)) || {};
|
||||||
|
var html = m.html || m.markdown || m.text || '';
|
||||||
|
// Truncate to keep the row compact
|
||||||
|
var text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
if (text.length > 120) text = text.slice(0, 117) + '…';
|
||||||
|
return CollabCentral.escapeHtml(text);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ data: 'totalRecipients' },
|
{ data: 'totalRecipients' },
|
||||||
{ data: 'scheduledFor' }
|
{ data: 'scheduledFor', render: function (v) { return renderDate(v); } },
|
||||||
],
|
],
|
||||||
searching: false,
|
searching: false,
|
||||||
info: false,
|
info: false,
|
||||||
ordering: false,
|
ordering: false,
|
||||||
paging: false
|
paging: false,
|
||||||
|
language: { emptyTable: 'No scheduled jobs.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#completedJobs').dataTable({
|
$('#completedJobs').dataTable({
|
||||||
ajax: {
|
ajax: {
|
||||||
url: '/CollabCentral/' + appName + '/jobs/list/completed',
|
url: '/CollabCentral/' + appName + '/jobs/list/completed',
|
||||||
dataSrc: ''
|
dataSrc: '',
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
data: 'jobId',
|
data: 'jobId',
|
||||||
render: function (jobId) {
|
render: function (jobId, type) {
|
||||||
if (jobId === undefined || jobId === null) return '';
|
if (type !== 'display') return jobId;
|
||||||
return '<a href="/CollabCentral/' + appName + '/jobDetail.html?jobId=' + encodeURIComponent(jobId) + '">#' + jobId + '</a>';
|
return jobDetailLink(appName, jobId, '#' + jobId);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ data: 'senderDisplayName' },
|
{ data: 'senderDisplayName' },
|
||||||
{ data: 'appName' },
|
{ data: 'appName' },
|
||||||
{ data: 'message.english.html' },
|
{ data: 'startTime', render: function (v) { return renderDate(v); } },
|
||||||
{ data: 'startTime' },
|
{ data: 'endTime', render: function (v) { return renderDate(v); } },
|
||||||
{ data: 'endTime' },
|
|
||||||
{ data: 'stats.totalTime' },
|
{ data: 'stats.totalTime' },
|
||||||
{ data: 'stats.webexTime' },
|
{
|
||||||
{ data: 'stats.averageTime' },
|
data: null,
|
||||||
{ data: 'stats.succeededMsgs' },
|
render: function (row, type) {
|
||||||
{ data: 'stats.totalMsgs' },
|
if (type !== 'display') return '';
|
||||||
{ data: 'stats.succeededMsgPct' }
|
return renderDelivered(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ data: 'stats.succeededMsgPct', render: function (v) { return renderSuccessRate(v); } },
|
||||||
],
|
],
|
||||||
searching: false,
|
order: [[3, 'desc']],
|
||||||
info: false,
|
pageLength: 25,
|
||||||
ordering: false,
|
lengthMenu: [10, 25, 50, 100],
|
||||||
paging: false
|
searching: true,
|
||||||
|
info: true,
|
||||||
|
paging: true,
|
||||||
|
language: { emptyTable: 'No completed jobs in the last 30 days.' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,308 +1,100 @@
|
||||||
<html>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>CollabCentral</title>
|
<title>CollabCentral</title>
|
||||||
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
<link id="appFavicon" rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<script>
|
<link rel="stylesheet" href="css/app.css">
|
||||||
let cookie = document.cookie;
|
|
||||||
if (cookie) {
|
|
||||||
} else {
|
|
||||||
window.location.href = "index.html";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 60%;
|
|
||||||
padding: 12px 20px;
|
|
||||||
margin: 8px 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field input {
|
|
||||||
border: 1px solid #87929D;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field label {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-top: 10px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
color: #2F3941;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: block;
|
|
||||||
background-color: tan;
|
|
||||||
width: 75%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#file-ip-1-preview {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.groups {
|
|
||||||
width: 1000px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#my-text-area {
|
|
||||||
height: 250px;
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer {
|
|
||||||
display: block;
|
|
||||||
width: 75%;
|
|
||||||
border: 1px solid #87929D;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
width: 75%;
|
|
||||||
height: auto;
|
|
||||||
margin: 10px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
width: 450px;
|
|
||||||
padding: 1.3rem;
|
|
||||||
min-height: 250px;
|
|
||||||
position: absolute;
|
|
||||||
top: 20%;
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 15px;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal .flex {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal input {
|
|
||||||
padding: 0.7rem 1rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal p {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #777;
|
|
||||||
margin: 0.4rem 0 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.8rem 1.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.8rem 1.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-open {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-close {
|
|
||||||
transform: translate(10px, -20px);
|
|
||||||
padding: 0.5rem 0.7rem;
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: block;
|
|
||||||
text-align: right;
|
|
||||||
align-content: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#appInfo {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
align-content: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#multi_option {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
vscomp-toggle-button {
|
|
||||||
padding: 10px 30px 10px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
||||||
<link rel="stylesheet" href="js/virtual-select.min.css" />
|
<link rel="stylesheet" href="js/virtual-select.min.css">
|
||||||
|
|
||||||
<script src="js/virtual-select.min.js"></script>
|
<script src="js/virtual-select.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body data-page="send" data-title-suffix="Send Message">
|
||||||
|
|
||||||
|
<header class="appHeader">
|
||||||
<div class="header">
|
<div class="appHeader__inner">
|
||||||
<div id="appInfo">
|
<div class="appHeader__brand">
|
||||||
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
<div id="appAvatar" class="appHeader__avatar"></div>
|
||||||
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
<div id="appLabel" class="appHeader__label">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;"><a id="jobLink" href=''>Monitor Jobs</a></div>
|
<nav class="appHeader__nav">
|
||||||
<div id="name" style="text-align: right;"></div>
|
<a href="./sendMessage.html" data-nav="send">Send Message</a>
|
||||||
|
<a href="./monitorJobs.html" data-nav="monitor">Monitor Jobs</a>
|
||||||
|
</nav>
|
||||||
|
<div class="appHeader__spacer"></div>
|
||||||
|
<div id="appUser" class="appHeader__user"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="unauthorizedPanel" class="panel hidden">
|
||||||
<div id="unauthorizedPanel" class="hidden" style="padding: 2em; max-width: 600px; margin: 2em auto; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
|
||||||
<h2>You don't have access to this bot</h2>
|
<h2>You don't have access to this bot</h2>
|
||||||
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="formPanel" class="form-input hidden">
|
<div id="formPanel" class="container hidden">
|
||||||
<form action="" id="sendMessage" enctype="multipart/form-data">
|
<div class="card">
|
||||||
<div class="form-field">
|
<h1 class="pageTitle">Compose a message</h1>
|
||||||
|
<p class="pageSubtitle">Pick your recipients, write the message, and send now or schedule it for later.</p>
|
||||||
|
|
||||||
<img id="file-ip-1-preview">
|
<form id="sendMessage" enctype="multipart/form-data">
|
||||||
<label for="uploadImage">Upload Image</label>
|
<div class="field">
|
||||||
<input type="file" id="uploadImage" accept="image/*" name="uploadImage"
|
|
||||||
onchange="showImagePreview(event);">
|
|
||||||
</div>
|
|
||||||
<label for="my-text-area">Message</label>
|
<label for="my-text-area">Message</label>
|
||||||
<textarea id="my-text-area"></textarea>
|
<textarea id="my-text-area"></textarea>
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="groups">
|
|
||||||
<label for="favGroups">Favorite Groups:</label>
|
|
||||||
<select id="favGroups" multiple name="favGroups" placeholder="Favorite Groups"
|
|
||||||
data-silent-initial-value-set="false">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="groups">
|
|
||||||
<label>New Groups:</label>
|
|
||||||
<select id="newGroups" multiple name="newGroups" placeholder="New Groups"
|
|
||||||
data-silent-initial-value-set="false">
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="people">
|
<div class="field">
|
||||||
<label for="uploadCSV">Upload CSV:</label>
|
<label for="uploadImage">Attach image (optional)</label>
|
||||||
|
<input type="file" id="uploadImage" accept="image/*" name="uploadImage" onchange="showImagePreview(event);">
|
||||||
|
<img id="file-ip-1-preview" alt="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="favGroups">Favorite groups</label>
|
||||||
|
<select id="favGroups" multiple name="favGroups" placeholder="Favorite groups"
|
||||||
|
data-silent-initial-value-set="false"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="newGroups">Additional groups</label>
|
||||||
|
<select id="newGroups" multiple name="newGroups" placeholder="Search all groups"
|
||||||
|
data-silent-initial-value-set="false"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="uploadCSV">Upload CSV of recipients (optional)</label>
|
||||||
<input type="file" id="uploadCSV" name="uploadCSV" accept="text/csv">
|
<input type="file" id="uploadCSV" name="uploadCSV" accept="text/csv">
|
||||||
</div>
|
<span class="hint">CSV of Webex person IDs or emails to add to the recipient list.</span>
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<div class="when">
|
|
||||||
<label for="meeting-time">Schedule for:</label>
|
|
||||||
<input type="datetime-local" id="scheduledFor" name="scheduledFor" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="submit">
|
|
||||||
<button id="submit" type="submit">Submit</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="scheduledFor">Schedule for (leave blank to send immediately)</label>
|
||||||
|
<input type="datetime-local" id="scheduledFor" name="scheduledFor">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formActions">
|
||||||
|
<button id="submit" type="submit" class="btn btn-primary">Review & send</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<section class="modal hidden">
|
|
||||||
<div class="flex">
|
|
||||||
<button class="btn-close" onclick="continueEditing()">X</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
|
|
||||||
|
<section class="modal hidden" aria-modal="true" role="dialog">
|
||||||
|
<button class="modal__close" onclick="continueEditing()" aria-label="Close">×</button>
|
||||||
<h3>Ready to send?</h3>
|
<h3>Ready to send?</h3>
|
||||||
<p id="modalMessage">
|
<p id="modalMessage">This message will be sent.</p>
|
||||||
This message will be sent to XX people.<br>
|
<div class="modal__actions">
|
||||||
Ready to send?
|
<button class="btn btn-secondary" onclick="continueEditing()">Keep editing</button>
|
||||||
</p>
|
<button class="btn btn-primary" onclick="submit()">Send</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn" onclick="continueEditing()">Continue Editing</button>
|
|
||||||
<button class="btn" onclick="submit()">Submit</button>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="overlay hidden"></div>
|
<div class="overlay hidden"></div>
|
||||||
|
|
||||||
</div>
|
<script src="js/app.js"></script>
|
||||||
<script src="sendMessage.js"></script>
|
<script src="sendMessage.js"></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,43 +1,53 @@
|
||||||
const listbox = document.querySelector('#list');
|
// Page script for sendMessage.html. Bootstrapping (auth cookie check, /info
|
||||||
const modal = document.querySelector(".modal");
|
// fetch, header rendering, active-nav highlight) all live in js/app.js;
|
||||||
const overlay = document.querySelector(".overlay");
|
// this file just wires the compose form.
|
||||||
const openModalBtn = document.querySelector(".btn-open");
|
|
||||||
const closeModalBtn = document.querySelector(".btn-close");
|
|
||||||
|
|
||||||
var pathArray = window.location.pathname.split('/'); //Gets the path of the URL called
|
var modal = document.querySelector('.modal');
|
||||||
var appName = pathArray[2]; //Gets the appName from the path.
|
var overlay = document.querySelector('.overlay');
|
||||||
|
|
||||||
|
var message = new EasyMDE({
|
||||||
|
element: document.getElementById('my-text-area'),
|
||||||
|
toolbar: [
|
||||||
|
'bold', 'italic', '|',
|
||||||
|
'heading-1', 'heading-2', 'heading-3', '|',
|
||||||
|
'code', 'unordered-list', 'ordered-list', 'link', 'quote', '|',
|
||||||
|
'preview'
|
||||||
|
],
|
||||||
|
maxHeight: '260px',
|
||||||
|
});
|
||||||
|
|
||||||
|
CollabCentral.init(function (ctx) {
|
||||||
|
// User is authenticated and authorized. Reveal the form + populate the
|
||||||
|
// group selectors from the backend.
|
||||||
|
document.getElementById('formPanel').classList.remove('hidden');
|
||||||
|
loadFavoriteGroups(ctx.appName);
|
||||||
|
loadNewGroups(ctx.appName);
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('sendMessage').onsubmit = function (event) {
|
document.getElementById('sendMessage').onsubmit = function (event) {
|
||||||
|
event.preventDefault();
|
||||||
event.preventDefault() // prevent form from posting without JS
|
var appName = CollabCentral.appName;
|
||||||
var xhttp = new XMLHttpRequest(); // create new AJAX request
|
var xhttp = new XMLHttpRequest();
|
||||||
|
|
||||||
|
|
||||||
xhttp.onreadystatechange = function () {
|
xhttp.onreadystatechange = function () {
|
||||||
if (this.readyState == this.DONE && this.status == 200) { // sucess from server
|
if (this.readyState === this.DONE && this.status === 200) {
|
||||||
result = JSON.parse(xhttp.response);
|
var result = JSON.parse(xhttp.response);
|
||||||
console.log(xhttp.responseText)
|
var scheduled = document.getElementById('scheduledFor').value;
|
||||||
console.log(xhttp.response);
|
var text = 'This message will be sent to <strong>' + result.memberList.length + '</strong> recipient(s).<br><br>';
|
||||||
var message = "I sent you a test message in your Webex Client for review.<br><br>";
|
if (scheduled) {
|
||||||
message += "Recipients: " + result.memberList.length + "<br><br>";
|
text += 'Scheduled for ' + CollabCentral.escapeHtml(scheduled) + '.';
|
||||||
if (document.getElementById("scheduledFor").value) {
|
|
||||||
message += "Scheduled for " + document.getElementById("scheduledFor").value;
|
|
||||||
} else {
|
} else {
|
||||||
message += "Message will be sent immediately.";
|
text += 'It will be sent immediately once you confirm.';
|
||||||
}
|
}
|
||||||
|
document.getElementById('modalMessage').innerHTML = text;
|
||||||
|
|
||||||
document.getElementById("modalMessage").innerHTML = message;
|
|
||||||
openModal();
|
openModal();
|
||||||
} else { // errors occured
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var formData = new FormData()
|
var formData = new FormData();
|
||||||
formData.append('uploadImage', document.getElementById('uploadImage').files[0]) // since inputs allow multi files submission, therefore files are in array
|
formData.append('uploadImage', document.getElementById('uploadImage').files[0]);
|
||||||
formData.append('message', message.value())
|
formData.append('message', message.value());
|
||||||
formData.append('uploadCSV', document.getElementById('uploadCSV').files[0]) // since inputs allow multi files submission, therefore files are in array
|
formData.append('uploadCSV', document.getElementById('uploadCSV').files[0]);
|
||||||
formData.append('appName', appName);
|
formData.append('appName', appName);
|
||||||
|
|
||||||
var selectedGroups = [];
|
var selectedGroups = [];
|
||||||
|
|
@ -47,176 +57,90 @@ document.getElementById('sendMessage').onsubmit = function (event) {
|
||||||
for (var group of document.querySelector('#newGroups').getSelectedOptions()) {
|
for (var group of document.querySelector('#newGroups').getSelectedOptions()) {
|
||||||
selectedGroups.push(group.value);
|
selectedGroups.push(group.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append('groups', selectedGroups);
|
formData.append('groups', selectedGroups);
|
||||||
formData.append('scheduledFor', document.getElementById("scheduledFor").value);
|
formData.append('scheduledFor', document.getElementById('scheduledFor').value);
|
||||||
xhttp.open("POST", "/CollabCentral/" + appName + "/jobs/edit")
|
|
||||||
console.log(xhttp);
|
xhttp.open('POST', '/CollabCentral/' + appName + '/jobs/edit');
|
||||||
xhttp.send(formData)
|
xhttp.send(formData);
|
||||||
}
|
};
|
||||||
|
|
||||||
function showImagePreview(event) {
|
function showImagePreview(event) {
|
||||||
if (event.target.files.length > 0) {
|
if (!event.target.files || !event.target.files.length) return;
|
||||||
var src = URL.createObjectURL(event.target.files[0]);
|
var src = URL.createObjectURL(event.target.files[0]);
|
||||||
var preview = document.getElementById("file-ip-1-preview");
|
var preview = document.getElementById('file-ip-1-preview');
|
||||||
preview.src = src;
|
preview.src = src;
|
||||||
preview.style.display = "block";
|
preview.style.display = 'block';
|
||||||
preview.style.maxHeight = "350px";
|
|
||||||
preview.style.maxWidth = "500px";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadFavoriteGroups(appName) {
|
||||||
let id = getCookie("id");
|
var el = document.getElementById('favGroups');
|
||||||
|
|
||||||
// Fetch bot metadata + authorization state, then either render the form or
|
|
||||||
// show the "not authorized" panel. All bot-specific labels and icons come from
|
|
||||||
// /info so adding a new bot doesn't require any front-end changes.
|
|
||||||
fetch('/CollabCentral/' + appName + '/info')
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(info => {
|
|
||||||
document.title = info.label;
|
|
||||||
document.getElementById("appLabel").innerHTML = info.label;
|
|
||||||
document.getElementById("name").innerHTML = getCookie("displayName") || "";
|
|
||||||
document.getElementById("appName").href = info.faviconUrl;
|
|
||||||
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/monitorJobs.html";
|
|
||||||
|
|
||||||
var appIcon = document.getElementById("appIcon");
|
|
||||||
appIcon.src = info.iconUrl;
|
|
||||||
appIcon.style.maxHeight = "150px";
|
|
||||||
appIcon.style.maxWidth = "150px";
|
|
||||||
|
|
||||||
if (!info.authorized) {
|
|
||||||
document.getElementById("unauthorizedPanel").classList.remove("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("formPanel").classList.remove("hidden");
|
|
||||||
loadFavoriteGroups();
|
|
||||||
loadNewGroups();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Failed to load /info:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadFavoriteGroups() {
|
|
||||||
var x = document.getElementById("favGroups");
|
|
||||||
fetch('/CollabCentral/' + appName + '/user/groups/list')
|
fetch('/CollabCentral/' + appName + '/user/groups/list')
|
||||||
.then(res => res.json())
|
.then(function (res) { return res.json(); })
|
||||||
.then((res) => {
|
.then(function (groups) {
|
||||||
for (var group of res) {
|
for (var i = 0; i < groups.length; i++) {
|
||||||
var option = document.createElement("option");
|
var option = document.createElement('option');
|
||||||
option.text = group.name;
|
option.text = groups[i].name;
|
||||||
option.value = group.id;
|
option.value = groups[i].id;
|
||||||
x.add(option);
|
el.add(option);
|
||||||
}
|
}
|
||||||
VirtualSelect.init({ ele: '#favGroups' });
|
VirtualSelect.init({ ele: '#favGroups' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNewGroups() {
|
function loadNewGroups(appName) {
|
||||||
var y = document.getElementById("newGroups");
|
var el = document.getElementById('newGroups');
|
||||||
fetch('/CollabCentral/' + appName + '/user/groups/find')
|
fetch('/CollabCentral/' + appName + '/user/groups/find')
|
||||||
.then(res => res.json())
|
.then(function (res) { return res.json(); })
|
||||||
.then((res) => {
|
.then(function (groups) {
|
||||||
for (var group of res) {
|
for (var i = 0; i < groups.length; i++) {
|
||||||
var option = document.createElement("option");
|
var option = document.createElement('option');
|
||||||
option.text = group.displayName;
|
option.text = groups[i].displayName;
|
||||||
option.value = group.id;
|
option.value = groups[i].id;
|
||||||
y.add(option);
|
el.add(option);
|
||||||
}
|
}
|
||||||
VirtualSelect.init({ ele: '#newGroups' });
|
VirtualSelect.init({ ele: '#newGroups' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = new EasyMDE({
|
|
||||||
element: document.getElementById('my-text-area'),
|
|
||||||
toolbar: ["bold", "italic", "|", "heading-1", "heading-2", "heading-3", "|", "code", "unordered-list", "ordered-list", "link", "quote", "|", "preview"],
|
|
||||||
maxHeight: "200px"
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
if (document.getElementById("scheduledFor").value) {
|
var appName = CollabCentral.appName;
|
||||||
fetch("/CollabCentral/" + appName + "/jobs/schedule", {
|
var scheduled = document.getElementById('scheduledFor').value;
|
||||||
method: "POST",
|
var url = scheduled
|
||||||
redirect: "follow"
|
? '/CollabCentral/' + appName + '/jobs/schedule'
|
||||||
})
|
: '/CollabCentral/' + appName + '/jobs/runNow';
|
||||||
.then(response => {
|
|
||||||
|
fetch(url, { method: 'POST', redirect: 'follow' })
|
||||||
|
.then(function (response) {
|
||||||
clearForm();
|
clearForm();
|
||||||
// HTTP 301 response
|
if (response.redirected) window.location.href = response.url;
|
||||||
if (response.redirected) {
|
|
||||||
window.location.href = response.url;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
console.info(err + " url: " + url);
|
console.error('Submit failed:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log("RunNow selected.");
|
|
||||||
fetch("/CollabCentral/" + appName + "/jobs/runNow", {
|
|
||||||
method: "POST",
|
|
||||||
redirect: "follow"
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
clearForm();
|
|
||||||
// HTTP 301 response
|
|
||||||
if (response.redirected) {
|
|
||||||
window.location.href = response.url;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.info(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function continueEditing() {
|
function continueEditing() { closeModal(); }
|
||||||
closeModal();
|
|
||||||
|
function openModal() {
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const openModal = function () {
|
function closeModal() {
|
||||||
modal.classList.remove("hidden");
|
modal.classList.add('hidden');
|
||||||
overlay.classList.remove("hidden");
|
overlay.classList.add('hidden');
|
||||||
};
|
}
|
||||||
|
|
||||||
const closeModal = function () {
|
overlay.addEventListener('click', closeModal);
|
||||||
modal.classList.add("hidden");
|
|
||||||
overlay.classList.add("hidden");
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.addEventListener("click", closeModal);
|
|
||||||
|
|
||||||
function clearForm() {
|
function clearForm() {
|
||||||
document.getElementById('uploadImage').innerHTML = "";
|
document.getElementById('uploadImage').value = '';
|
||||||
message.value("");
|
message.value('');
|
||||||
document.getElementById('uploadCSV').innerHTML = "";
|
document.getElementById('uploadCSV').value = '';
|
||||||
document.getElementById('favGroups').selected = null;
|
document.getElementById('scheduledFor').value = '';
|
||||||
document.getElementById('newGroups').selected = null;
|
var preview = document.getElementById('file-ip-1-preview');
|
||||||
document.getElementById('scheduledFor').innerHTML = "";
|
preview.removeAttribute('src');
|
||||||
|
preview.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue