Skip to content

Instantly share code, notes, and snippets.

@metasong
Last active June 21, 2023 23:19
Show Gist options
  • Save metasong/d4b488cea7d7dc02980bd54a6aa3e6b7 to your computer and use it in GitHub Desktop.
Save metasong/d4b488cea7d7dc02980bd54a6aa3e6b7 to your computer and use it in GitHub Desktop.
SeqSignals.js

Description

if you are using Seq and want to save/share/manage your commonly used searching filter signals, then you are like me and this would helpful.😉

Extended Functions

as highlighted, buttons for manipulating all signals are added:

  • delete all signals
  • import signals from file
  • export selected active signals
  • export all signals
  • rename signal group name

Tutorial to run the script:

There are two way to use it

  1. Open Seq application, Press F12 to open the browser's DevTool, just paste the script into the console and run it.

  2. Automatically run the script when open the Seq application

    if you can only see the script content and can not see the Tempermonkey installation page, make sure your tampermonkey extension is installed successfully in your browser.

    image

Gist Url

https://gist.github.com/metasong/d4b488cea7d7dc02980bd54a6aa3e6b7#file-readme-md

// ==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
})();
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::TLS12
Invoke-WebRequest -UseBasicParsing -Method 'GET' -Uri 'http://localhost:5341/api/signals/signal-98' -Headers @{'Accept'= 'application/json'}
Invoke-WebRequest -UseBasicParsing -Method 'GET' -Uri 'http://localhost:5341/api/signals/resources' -Headers @{'Accept'= 'application/json'}
Invoke-WebRequest -UseBasicParsing -Method 'GET' -Uri 'http://localhost:5341/api/signals/?ownerId=user-admin&shared=true' -Headers @{'Accept'= 'application/json'}
Invoke-WebRequest -UseBasicParsing -Method 'GET' -Uri 'https://drillops.rig/log/api/signals/?ownerId=user-admin&shared=true' -Headers @{'Accept'= 'application/json, text/plain, */*';}
Invoke-WebRequest -Body @{username = '1'; password = '1'; target = ''} -Method 'POST' -Uri 'https://drillops.rig/auth/login' -Headers @{Accept = 'application/json, text/plain, */*'; 'Content-Type'= 'application/json'} -SessionVariable 'Session'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment