|
// ==UserScript== |
|
// @name Seq signals |
|
// @namespace http://tampermonkey.net/ |
|
// @version 1.3 |
|
// @description add functions to manipulate signals: delete, import, export actives, export all, rename group. |
|
// the readme: https://dev.azure.com/JSong2020/_git/SeqSignals?path=/readme.md&version=GBmaster&_a=preview |
|
// @author Jianzhong Song |
|
// @match http://localhost:5341/ |
|
// @match https://drillops.rig/log/ |
|
// @match https://*/log/ |
|
// @icon https://datalust.co/favicon.png |
|
// @grant GM_notification |
|
// @note for seq front version: Seq 2021.1.5307 |
|
// ==/UserScript== |
|
|
|
// https://docs.datalust.co/docs/server-http-api |
|
|
|
(async () => { |
|
let notificationTimeout = 6000 //ms |
|
let url = location.origin + location.pathname.replace(/\/$/, '') |
|
|
|
//#region server api |
|
function linkRequestEventWithPromise(request, resolve, reject) { |
|
request.onload = e => { |
|
if (request.readyState == 4 && request.status >= 200 && request.status < 300) // DONE |
|
resolve(e) |
|
else |
|
reject(e) |
|
} |
|
request.onerror = reject |
|
} |
|
|
|
let createSignal = (title, filters, groupName, columns, isWatched) => new Promise((resolve, reject) => { |
|
var theUrl = `${url}/api/signals/` // ?shared=true |
|
var request = new XMLHttpRequest() |
|
request.open("POST", theUrl) |
|
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") |
|
linkRequestEventWithPromise(request, resolve, reject) |
|
request.send(JSON.stringify( |
|
{ |
|
"Title": title, |
|
"Description": null, |
|
"Filters": filters, |
|
"Columns": columns, |
|
"IsWatched": isWatched, |
|
"Id": null, |
|
"Grouping": groupName ? "Explicit" : "Inferred", |
|
"ExplicitGroupName": groupName, |
|
// null or "user-admin" |
|
// null means shared; when use shared, the unselected signals would always hidden in 'more'. |
|
// but the 'user-admin' would show it once it has been used for one time |
|
"OwnerId": "user-admin", |
|
"Links": { |
|
"Create": "api/signals/" |
|
} |
|
} |
|
)) |
|
}) |
|
|
|
let updateSignal = signal => new Promise((resolve, reject) => { |
|
var theUrl = `${url}/api/signals/${signal.Id}` |
|
var request = new XMLHttpRequest() |
|
request.open("PUT", theUrl) |
|
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") |
|
linkRequestEventWithPromise(request, resolve, reject) |
|
request.send(JSON.stringify(signal)) |
|
}) |
|
|
|
let deleteSignal = (signalId, baseUrl) => new Promise((resolve, reject) => { |
|
var request = new XMLHttpRequest() |
|
var theUrl = `${baseUrl}/api/signals/${signalId}?version=1` |
|
request.open("DELETE", theUrl) |
|
linkRequestEventWithPromise(request, resolve, reject) |
|
request.send() |
|
}) |
|
|
|
let getAllSignals = baseUrl => new Promise((resolve, reject) => { |
|
var xhr = new XMLHttpRequest() |
|
xhr.open('GET', `${baseUrl}/api/signals/?ownerId=user-admin&shared=true`, true) |
|
linkRequestEventWithPromise(xhr, resolve, reject) |
|
xhr.send() |
|
}) |
|
//#endregion |
|
|
|
//#region button event handlers |
|
function download(text, isActive = false) { |
|
let a = document.createElement("a") |
|
a.href = "data:text," + text |
|
let fileName = `SeqSignals_${isActive ? 'Active' : 'All'}.json` |
|
a.download = fileName |
|
a.click() |
|
GM_notification({ text: 'Downloaded ' + fileName, title: `Download ${isActive ? 'Active' : 'All'}`, silent: true, timeout: notificationTimeout }) |
|
} |
|
|
|
async function downloadAllSignals() { |
|
let e = await getAllSignals(url) |
|
download(e.target.responseText) |
|
} |
|
|
|
async function deleteAllSignals() { |
|
if (!confirm('Are you want to delete ALL the signals?')) return |
|
|
|
let e = await getAllSignals(url) |
|
let signals = JSON.parse(e.target.responseText) |
|
let count = signals.length |
|
if (count == 0) { |
|
GM_notification({ text: "NO signal to delete!", title: 'Delete All Signals', timeout: notificationTimeout }) |
|
return |
|
} |
|
signals.forEach(async signal => { |
|
await deleteSignal(signal.Id, url) |
|
if (--count == 0) // all delete request handled |
|
{ |
|
GM_notification({ text: 'All signals are deleted', title: 'Signal Delete', silent: true, timeout: notificationTimeout }) |
|
location.href = url // except reload, also remove the signals query filters: ?signal=signal-645,signal-651 |
|
} |
|
}) |
|
} |
|
|
|
function isSignalEqual(sig1, sig2) { |
|
if (sig1.Title !== sig2.Title || |
|
sig1.Filters.length !== sig2.Filters.length || |
|
sig1.Columns.length !== sig2.Columns.length |
|
) return false |
|
|
|
let filtersEqual = sig1.Filters.every(s1f => sig2.Filters.some(s2f => s2f.Filter == s1f.Filter && s2f.FilterNonStrict == s1f.FilterNonStrict)) |
|
if (!filtersEqual) return false |
|
|
|
filtersEqual = sig2.Filters.every(s2f => sig1.Filters.some(s1f => s1f.Filter == s2f.Filter && s1f.FilterNonStrict == s2f.FilterNonStrict)) |
|
if (!filtersEqual) return false |
|
|
|
let columnsEqual = sig1.Columns.every(s1c => sig2.Columns.some(s2c => s2c.Expression == s1c.Expression)) |
|
if (!columnsEqual) return false |
|
|
|
columnsEqual = sig2.Columns.every(s2c => sig1.Columns.some(s1c => s1c.Expression == s2c.Expression)) |
|
if (!columnsEqual) return false |
|
|
|
return true |
|
} |
|
|
|
function addInputFileDialog() { |
|
let input = document.createElement('input') |
|
input.setAttribute('multiple', true) |
|
input.type = 'file' |
|
input.id = 'import_seq_signals' |
|
input.style.position = 'absolute' |
|
input.style.top = '-100px' |
|
document.body.appendChild(input) |
|
return input |
|
} |
|
|
|
const inputFile = addInputFileDialog() |
|
|
|
const readJsonFileAsync = (file) => new Promise((resolve, reject) => { |
|
var reader = new FileReader() |
|
reader.readAsText(file, "UTF-8") |
|
reader.onload = resolve |
|
reader.onerror = reject |
|
}); |
|
|
|
async function importSignals() { |
|
inputFile.addEventListener('change', async function handler() { |
|
inputFile.removeEventListener('change', handler) |
|
let files = inputFile.files |
|
if (files.length === 0) { |
|
GM_notification({ text: 'No signal files selected!', title: 'Signal Import', timeout: notificationTimeout }) |
|
return |
|
} |
|
|
|
let evt = await getAllSignals(url) |
|
let signals_now = JSON.parse(evt.target.responseText) |
|
const lastFile = files[files.length - 1]; |
|
for (const file of files) { |
|
try { |
|
const evt = await readJsonFileAsync(file) |
|
let signals = JSON.parse(evt.target.result) |
|
let count = signals.length |
|
for (const signal of signals) { |
|
if (!signals_now.some(s => isSignalEqual(s, signal))) { |
|
await createSignal(signal.Title, signal.Filters, signal.ExplicitGroupName, signal.Columns, signal.IsWatched) |
|
signals_now.push(signal) |
|
} |
|
if (--count === 0 && file === lastFile) { |
|
GM_notification({ text: 'Signals imported!', title: 'Signal Import', silent: true, timeout: notificationTimeout }) |
|
location.reload() //all create request handled |
|
} |
|
} |
|
} catch { |
|
GM_notification({ text: `Error reading signal file: ${file}!`, title: 'Signal Import', timeout: notificationTimeout }) |
|
} |
|
} |
|
|
|
}) |
|
|
|
const evt = new MouseEvent('click', { bubbles: true, cancelable: false }); |
|
inputFile.dispatchEvent(evt, true) |
|
} |
|
|
|
// http://localhost:5341/#/events?signal=signal-645,signal-651,signal-655,(signal-669~signal-668) |
|
function getActiveSignalIds() { |
|
let signals = [] |
|
let signalsStr = location.hash.split('=')[1]; |
|
signalsStr.split(',').forEach(signalGroup => { |
|
if (!signalGroup) return |
|
|
|
let group = signalGroup.match(/\s*\((.*)\)\s*/) |
|
if (group) { |
|
let groupSignalsStr = group[1] // inside () |
|
let groupSignals = groupSignalsStr.split('~') |
|
groupSignals.forEach(sig => signals.push(sig)) |
|
} else { |
|
signals.push(signalGroup) |
|
} |
|
}) |
|
return signals |
|
} |
|
//#endregion |
|
|
|
//#region UI |
|
let deleteIcon = `<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"></path></svg>` |
|
let exportIcon = `<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"></path></svg>` |
|
let importIcon = `<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464,128H272L208,64H48A48,48,0,0,0,0,112V400a48,48,0,0,0,48,48H464a48,48,0,0,0,48-48V176A48,48,0,0,0,464,128ZM359.5,296a16,16,0,0,1-16,16h-64v64a16,16,0,0,1-16,16h-16a16,16,0,0,1-16-16V312h-64a16,16,0,0,1-16-16V280a16,16,0,0,1,16-16h64V200a16,16,0,0,1,16-16h16a16,16,0,0,1,16,16v64h64a16,16,0,0,1,16,16Z"></path></svg>` |
|
let exportSelectedIcon = `<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 480H48c-26.51 0-48-21.49-48-48V80c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48zM128 120c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zm0 96c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zm0 96c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zm288-136v-32c0-6.627-5.373-12-12-12H204c-6.627 0-12 5.373-12 12v32c0 6.627 5.373 12 12 12h200c6.627 0 12-5.373 12-12zm0 96v-32c0-6.627-5.373-12-12-12H204c-6.627 0-12 5.373-12 12v32c0 6.627 5.373 12 12 12h200c6.627 0 12-5.373 12-12zm0 96v-32c0-6.627-5.373-12-12-12H204c-6.627 0-12 5.373-12 12v32c0 6.627 5.373 12 12 12h200c6.627 0 12-5.373 12-12z"></path></svg>` |
|
function creatSignalButton(svg, title, action) { |
|
var d = document.createElement('a') |
|
d.classList.add('add-list-item') |
|
d.title = title |
|
d.addEventListener('click', action, false) |
|
d.innerHTML = `<i class="icon">${svg}</i>` |
|
return d |
|
} |
|
|
|
async function exportActiveSignals() { |
|
let activeSignalIds = getActiveSignalIds() |
|
if (activeSignalIds.length == 0) { |
|
GM_notification({ text: 'Please select signals!', title: 'No Active Signals', timeout: notificationTimeout }) |
|
return |
|
} |
|
|
|
let activeSignals = [] |
|
let e = await getAllSignals(url) |
|
let signals = JSON.parse(e.target.responseText) |
|
signals.forEach(signal => { |
|
if (activeSignalIds.some(a => signal.Id == a)) { |
|
activeSignals.push(signal) |
|
} |
|
}) |
|
download(JSON.stringify(activeSignals), true) |
|
} |
|
|
|
function enableEditSignalGroupName() { |
|
setInterval(() => { |
|
let groupTitleElements = document.querySelectorAll('div.group-header a.item-title') |
|
groupTitleElements.forEach(e => { |
|
if (!e.getAttribute('contenteditable')) { |
|
e.setAttribute('contenteditable', true) |
|
e.addEventListener('focus', event => { |
|
event.target.originalName = event.target.innerText |
|
// console.log(event.target.originalName) |
|
}) |
|
// e.removeAttribute('ng-click') not work |
|
e.addEventListener("keyup", async event => { |
|
if (event.keyCode === 13) { // enter |
|
event.preventDefault() |
|
event.target.blur() |
|
let newGroupName = event.target.innerText.replace(/(\r\n|\r|\n)/g, '').trim() |
|
event.target.innerText = newGroupName |
|
let xhrEvent = await getAllSignals(url) |
|
let signals = JSON.parse(xhrEvent.target.responseText) |
|
signals.forEach(async signal => { |
|
if (signal.ExplicitGroupName === event.target.originalName) { |
|
signal.ExplicitGroupName = newGroupName |
|
await updateSignal(signal) |
|
GM_notification({ text: `Group name changed to ${newGroupName}!`, title: 'Edit Group Name', timeout: notificationTimeout }) |
|
} |
|
}) |
|
} else if (event.keyCode === 27) { // escape |
|
event.preventDefault() |
|
event.target.blur() |
|
event.target.innerText = event.target.originalName |
|
GM_notification({ text: 'Cancel editing!', title: 'Edit Group Name', timeout: notificationTimeout }) |
|
|
|
} |
|
}); |
|
} |
|
}) |
|
}, 1888) // wait for all signals rendered. |
|
} |
|
|
|
let buttons = [] |
|
buttons.push(creatSignalButton(deleteIcon, 'Delete ALL signals', deleteAllSignals)) |
|
buttons.push(creatSignalButton(importIcon, 'Import your signals', importSignals)) |
|
buttons.push(creatSignalButton(exportSelectedIcon, 'Export selected active signals', exportActiveSignals)) |
|
buttons.push(creatSignalButton(exportIcon, 'Export all signals', downloadAllSignals)) |
|
|
|
let timer = setInterval(() => { |
|
var b = document.querySelector('.signal-list .subhead') |
|
if (b) { |
|
clearInterval(timer) |
|
let addButton = b.firstElementChild |
|
buttons.forEach(button => b.insertBefore(button, addButton)) |
|
enableEditSignalGroupName() |
|
|
|
} |
|
|
|
}, 888) |
|
//#endregion |
|
})(); |