frontend: Add linting for ts code

This commit is contained in:
Tom 2023-07-13 13:51:18 +02:00
parent ddb4bb3546
commit 16fa721048
18 changed files with 2299 additions and 469 deletions

View file

@ -1,4 +1,4 @@
.PHONY: clean all deps live .PHONY: clean all deps live tslint
live: live:
sudo CGO_ENABLED=0 go run ./cmd/wdcli $(ARGS) sudo CGO_ENABLED=0 go run ./cmd/wdcli $(ARGS)
@ -9,6 +9,9 @@ wdcli:
go generate ./internal/dis/component/control/static/ go generate ./internal/dis/component/control/static/
CGO_ENABLED=0 go build -o ./wdcli ./cmd/wdcli CGO_ENABLED=0 go build -o ./wdcli ./cmd/wdcli
tslint:
cd internal/dis/component/server/assets/ && yarn ts-standard
deps: internal/dis/component/server/assets/node_modules deps: internal/dis/component/server/assets/node_modules
internal/dis/component/server/assets/node_modules: internal/dis/component/server/assets/node_modules:

View file

@ -61,6 +61,9 @@ func (am ActionMap) Handle(conn httpx.WebSocketConnection) (name string, err err
} else { } else {
result.Success = false result.Success = false
result.Message = err.Error() result.Message = err.Error()
if result.Message == "" {
result.Message = "unspecified error"
}
} }
// encode the result message to json! // encode the result message to json!

View file

@ -1,7 +1,7 @@
import { Parcel } from "@parcel/core" import { Parcel } from '@parcel/core'
import { mkdir, rm, writeFile, readFile, unlink, rmdir, } from "fs/promises" import { mkdir, rm, writeFile, readFile, unlink } from 'fs/promises'
import { join } from "path" import { join } from 'path'
import { parse as parseHTML } from 'node-html-parser'; import { parse as parseHTML } from 'node-html-parser'
import { spawnSync } from 'child_process' import { spawnSync } from 'child_process'
// //
@ -36,31 +36,28 @@ await Promise.all([
]) ])
console.log(' Done.') console.log(' Done.')
// //
// Write the disclaimer // Write the disclaimer
// //
process.stdout.write('Generating legal disclaimer ...') process.stdout.write('Generating legal disclaimer ...')
const disclaimer = await new Promise((r, e) => { const disclaimer = await new Promise((resolve, reject) => {
var child = spawnSync("yarn", ["licenses", "generate-disclaimer"], { encoding : 'utf8' }); const child = spawnSync('yarn', ['licenses', 'generate-disclaimer'], { encoding: 'utf8' })
if (child.error) { if (child.error) {
e(child.stderr) reject(child.stderr)
return return
} }
r(child.stdout) resolve(child.stdout)
}); })
console.log(' Done.') console.log(' Done.')
process.stdout.write(`Writing ${DEST_DISCLAIMER} ...`) process.stdout.write(`Writing ${DEST_DISCLAIMER} ...`)
await writeFile(DEST_DISCLAIMER, disclaimer) await writeFile(DEST_DISCLAIMER, disclaimer)
console.log(' Done.') console.log(' Done.')
// //
// WRITE ENTRY POINTS // WRITE ENTRY POINTS
// //
@ -68,20 +65,20 @@ console.log(' Done.')
process.stdout.write('Collecting entry points ') process.stdout.write('Collecting entry points ')
const entries = await Promise.all(ENTRYPOINTS.map(async (name) => { const entries = await Promise.all(ENTRYPOINTS.map(async (name) => {
const entry = { const entry = {
'name': name, name,
'bundleName': name + '.html', bundleName: name + '.html',
'src': join(ENTRY_DIR, name + '.html'), src: join(ENTRY_DIR, name + '.html')
} }
const content = ` const content = `
<script type='module' src='../src/base/index.ts'></script> <script type='module' src='../src/base/index.ts'></script>
<script type='module' src='../src/entry/${name}/index.ts'></script> <script type='module' src='../src/entry/${name}/index.ts'></script>
<link rel='stylesheet' href='../src/entry/${name}/index.css'> <link rel='stylesheet' href='../src/entry/${name}/index.css'>
`; `
await writeFile(entry.src, content) await writeFile(entry.src, content)
process.stdout.write('.') process.stdout.write('.')
return entry; return entry
})) }))
console.log(' Done.') console.log(' Done.')
@ -102,10 +99,10 @@ const bundler = new Parcel({
distDir: DIST_DIR, distDir: DIST_DIR,
publicUrl: PUBLIC_DIR, publicUrl: PUBLIC_DIR,
engines: { engines: {
browsers: "defaults", browsers: 'defaults'
} }
} }
}); })
const { bundleGraph } = await bundler.run() const { bundleGraph } = await bundler.run()
console.log(' Done.') console.log(' Done.')
@ -158,7 +155,7 @@ var Disclaimer string
const Public = ${JSON.stringify(PUBLIC_DIR)} const Public = ${JSON.stringify(PUBLIC_DIR)}
${goAssets} ${goAssets}
`; `
await writeFile(DEST_FILE, goSource) await writeFile(DEST_FILE, goSource)
console.log(' Done.') console.log(' Done.')

View file

@ -11,5 +11,17 @@
"node-html-parser": "^6.1.1", "node-html-parser": "^6.1.1",
"parcel": "^2.7.0", "parcel": "^2.7.0",
"purecss": "^2.1.0" "purecss": "^2.1.0"
},
"devDependencies": {
"ts-standard": "^12.0.2",
"typescript": "^5.1.6"
},
"ts-standard": {
"ignore": [
"dist",
"node_modules",
".entry-cache",
".parcel-cache"
]
} }
} }

View file

@ -1,4 +1,4 @@
import "purecss/build/pure.css" import 'purecss/build/pure.css'
import "purecss/build/grids-responsive.css" import 'purecss/build/grids-responsive.css'
import "./index.css" import './index.css'

View file

@ -1,15 +1,15 @@
// setup highlighting // setup highlighting
import "~/src/lib/highlight" import '~/src/lib/highlight'
// setup remote actions // setup remote actions
import setup from "~/src/lib/remote" import setup from '~/src/lib/remote'
setup();
// include the user styles! // include the user styles!
import "../User/index.ts" import '../User/index.ts'
import "../User/index.css" import '../User/index.css'
// highlight everything // highlight everything
import "highlight.js/styles/default.css" import 'highlight.js/styles/default.css'
import highlightJs from "highlight.js" import highlightJs from 'highlight.js'
highlightJs.highlightAll(); setup()
highlightJs.highlightAll()

View file

@ -1,24 +1,23 @@
import "../Admin/index.ts" import '../Admin/index.ts'
import "../Admin/index.css" import '../Admin/index.css'
import { Provision } from "~/src/lib/remote/api" import { Provision } from '~/src/lib/remote/api'
const provision = document.getElementById("provision") as HTMLFormElement; const provision = document.getElementById('provision') as HTMLFormElement
const slug = document.getElementById("slug") as HTMLInputElement; const slug = document.getElementById('slug') as HTMLInputElement
const php = document.getElementById("php") as HTMLSelectElement; const php = document.getElementById('php') as HTMLSelectElement
const opcacheDevelopment = document.getElementById("opcacheDevelopment") as HTMLInputElement; const opcacheDevelopment = document.getElementById('opcacheDevelopment') as HTMLInputElement
// add an event handler to open the modal form! // add an event handler to open the modal form!
provision.addEventListener('submit', (evt) => { provision.addEventListener('submit', (evt) => {
evt.preventDefault(); evt.preventDefault()
Provision({ Slug: slug.value, System: { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked } }) Provision({ Slug: slug.value, System: { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked } })
.then(slug => { .then(slug => {
location.href = "/admin/instance/" + slug; location.href = '/admin/instance/' + slug
}) })
.catch((e) => {console.error(e); location.reload()}); .catch((e) => { console.error(e); location.reload() })
}) })
// enable the form! // enable the form!
provision.querySelector('fieldset')?.removeAttribute('disabled'); provision.querySelector('fieldset')?.removeAttribute('disabled')

View file

@ -1,24 +1,23 @@
import "../Admin/index.ts" import '../Admin/index.ts'
import "../Admin/index.css" import '../Admin/index.css'
import { Rebuild } from "~/src/lib/remote/api" import { Rebuild } from '~/src/lib/remote/api'
const slug = document.getElementById("slug") as HTMLInputElement const slug = document.getElementById('slug') as HTMLInputElement
const provision = document.getElementById("provision") as HTMLFormElement; const provision = document.getElementById('provision') as HTMLFormElement
const php = document.getElementById("php") as HTMLSelectElement; const php = document.getElementById('php') as HTMLSelectElement
const opcacheDevelopment = document.getElementById("opcacheDevelopment") as HTMLInputElement; const opcacheDevelopment = document.getElementById('opcacheDevelopment') as HTMLInputElement
// add an event handler to open the modal form! // add an event handler to open the modal form!
provision.addEventListener('submit', (evt) => { provision.addEventListener('submit', (evt) => {
evt.preventDefault(); evt.preventDefault()
Rebuild(slug.value, { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked }) Rebuild(slug.value, { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked })
.then(slug => { .then(slug => {
location.href = "/admin/instance/" + slug; location.href = '/admin/instance/' + slug
}) })
.catch((e) => {console.error(e); location.reload()}); .catch((e) => { console.error(e); location.reload() })
}) })
// enable the form! // enable the form!
provision.querySelector('fieldset')?.removeAttribute('disabled'); provision.querySelector('fieldset')?.removeAttribute('disabled')

View file

@ -1,2 +1,2 @@
import "~/src/lib/copy" import '~/src/lib/copy'
import "~/src/lib/reveal" import '~/src/lib/reveal'

View file

@ -1,18 +1,18 @@
import "./index.css" import './index.css'
/** Adapted from http://blog.parkermoore.de/2014/08/01/header-anchor-links-in-vanilla-javascript-for-github-pages-and-jekyll/ */ /** Adapted from http://blog.parkermoore.de/2014/08/01/header-anchor-links-in-vanilla-javascript-for-github-pages-and-jekyll/ */
const anchorForId = (id: string) => { const anchorForId = (id: string): HTMLAnchorElement => {
const anchor = document.createElement("a") const anchor = document.createElement('a')
anchor.className = "header-link" anchor.className = 'header-link'
anchor.href = "#" + id anchor.href = '#' + id
anchor.innerHTML = "#" anchor.innerHTML = '#'
return anchor return anchor
} }
const linkifyAnchors = (level: number) => { const linkifyAnchors = (level: number): void => {
const headers = document.getElementsByTagName("h" + level); const headers = document.getElementsByTagName('h' + level.toString())
Array.from(headers).forEach((header) => { Array.from(headers).forEach((header) => {
if (typeof header.id === "undefined" || header.id === "") return if (typeof header.id === 'undefined' || header.id === '') return
header.appendChild(anchorForId(header.id)) header.appendChild(anchorForId(header.id))
}) })
} }

View file

@ -1,8 +1,13 @@
import "./index.css" import './index.css'
import { discard } from '~/src/lib/discard'
document.querySelectorAll('.copy').forEach((elem: Element) => { document.querySelectorAll('.copy').forEach((elem: Element) => {
elem.addEventListener('click', () => { elem.addEventListener('click', () => {
if (!navigator.clipboard) return; // Check if the clipboard api is supported
navigator.clipboard.writeText((elem as HTMLElement).innerText); // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!navigator.clipboard) return
discard(navigator.clipboard.writeText((elem as HTMLElement).innerText))
}) })
}) })

View file

@ -0,0 +1,13 @@
import { Mutex, MutexInterface } from 'async-mutex'
const error = console.error.bind(console)
/** discard discards the result of a promise, or logs an error if it occurs */
export function discard<T> (p: Promise<T>): void {
p.then((): void => {}).catch(error)
}
/** runs worker exclusively on m, and discards the resulting promise */
export function runMutexExclusive<T> (m: Mutex, worker: MutexInterface.Worker<T>): void {
discard(m.runExclusive(worker))
}

View file

@ -1,7 +1,7 @@
import dayjs from "dayjs" import dayjs from 'dayjs'
const types: Record<string, (element: HTMLElement) => HTMLElement | string> = { const types: Record<string, (element: HTMLElement) => HTMLElement | string> = {
"date": (element) => { date: (element) => {
const value = dayjs(element.innerText); const value = dayjs(element.innerText)
const text = value.format('YYYY-MM-DD HH:mm:ss ([UTC]Z)') const text = value.format('YYYY-MM-DD HH:mm:ss ([UTC]Z)')
// if the date is the zero date, then it is assumed to be invalid // if the date is the zero date, then it is assumed to be invalid
@ -13,32 +13,32 @@ const types: Record<string, (element: HTMLElement) => HTMLElement | string> = {
} }
return text return text
}, },
"path": (element) => { path: (element) => {
const text = element.innerText.split("/"); const text = element.innerText.split('/')
return text[text.length - 1]; return text[text.length - 1]
}, },
"pathbuilder": (element) => { pathbuilder: (element) => {
// create a link and get the blob // create a link and get the blob
const filename = (element.getAttribute('data-name') ?? 'pathbuilder') + ".xml" const filename = (element.getAttribute('data-name') ?? 'pathbuilder') + '.xml'
const [link, blob] = make_download_link(filename, element.innerText, "application/xml") const [link, blob] = makeDownloadLink(filename, element.innerText, 'application/xml')
link.className = "pure-button" link.className = 'pure-button'
const title = filename + ' (' + blob.size + ' Bytes)'; const title = filename + ' (' + blob.size.toString() + ' Bytes)'
link.append(title) link.append(title)
return link return link
} }
} }
const make_download_link = (filename: string, content: string, type: string): [HTMLAnchorElement, Blob] => { const makeDownloadLink = (filename: string, content: string, type: string): [HTMLAnchorElement, Blob] => {
const blob = new Blob( const blob = new Blob(
[content], [content],
{ {
type: type ?? "text/plain" type: type ?? 'text/plain'
} }
); )
const link = document.createElement("a") const link = document.createElement('a')
link.target = "_blank" link.target = '_blank'
link.download = filename link.download = filename
link.href = URL.createObjectURL(blob) link.href = URL.createObjectURL(blob)
@ -46,16 +46,16 @@ const make_download_link = (filename: string, content: string, type: string): [H
} }
Object.keys(types).forEach(key => { Object.keys(types).forEach(key => {
const f = types[key]; const f = types[key]
const elements = document.querySelectorAll("code." + key) as NodeListOf<HTMLElement> const elements = document.querySelectorAll<HTMLElement>('code.' + key)
elements.forEach(element => { elements.forEach(element => {
const newElement = f(element) const newElement = f(element)
if (typeof newElement === 'string') { if (typeof newElement === 'string') {
element.innerHTML = "" element.innerHTML = ''
element.appendChild(document.createTextNode(newElement)) element.appendChild(document.createTextNode(newElement))
return return
} }
element.parentNode!.replaceChild(newElement, element) element.parentNode?.replaceChild(newElement, element)
}) })
}) })

View file

@ -1,4 +1,4 @@
import { createModal } from "~/src/lib/remote" import { createModal } from '~/src/lib/remote'
/** /**
* Flags to provision a new system. * Flags to provision a new system.
@ -10,41 +10,41 @@ interface ProvisionFlags {
} }
interface System { interface System {
PHP: string; PHP: string
OpCacheDevelopment: boolean OpCacheDevelopment: boolean
} }
/** Rebuild the specified instance */ /** Rebuild the specified instance */
export async function Rebuild(slug: string, system: System): Promise<string> { export async function Rebuild (slug: string, system: System): Promise<string> {
return new Promise((rs, rj) => { return await new Promise((resolve, reject) => {
createModal("rebuild", [slug, JSON.stringify(system)], { createModal('rebuild', [slug, JSON.stringify(system)], {
bufferSize: 0, bufferSize: 0,
onClose: (success: boolean, message?: string) => { onClose: (success: boolean, message?: string) => {
if (!success) { if (!success) {
rj(new Error(message ?? "unspecified error")) reject(new Error(message ?? 'unspecified error'))
return; return
} }
rs(slug); resolve(slug)
}, }
})
}) })
});
} }
/** Provision provisions a new instance */ /** Provision provisions a new instance */
export async function Provision(flags: ProvisionFlags): Promise<string> { export async function Provision (flags: ProvisionFlags): Promise<string> {
// open a modal to provision a new instance // open a modal to provision a new instance
return new Promise((rs, rj) => { return await new Promise((resolve, reject) => {
createModal("provision", [JSON.stringify(flags)], { createModal('provision', [JSON.stringify(flags)], {
bufferSize: 0, bufferSize: 0,
onClose: (success: boolean, message?: string) => { onClose: (success: boolean, message?: string) => {
if (!success) { if (!success) {
rj(new Error(message ?? "unspecified error")) reject(new Error(message ?? 'unspecified error'))
return; return
} }
rs(flags.Slug); resolve(flags.Slug)
}, }
})
}) })
});
} }

View file

@ -1,27 +1,27 @@
import "./index.css" import './index.css'
import callServerAction, { ResultMessage } from './proto' import callServerAction, { ResultMessage } from './proto'
type Println = ((line: string, flush?: boolean) => void) & { type Println = ((line: string, flush?: boolean) => void) & {
paintedFrames: number; paintedFrames: number
missedFrames: number; missedFrames: number
} }
/** /**
* makeTextBuffer returns a println() function that efficiently writes text into target, and keeps at most size elements in the traceback. * makeTextBuffer returns a println() function that efficiently writes text into target, and keeps at most size elements in the traceback.
* scrollContainer is used to scroll on every painted update. * scrollContainer is used to scroll on every painted update.
*/ */
function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size: number): Println { function makeTextBuffer (target: HTMLElement, scrollContainer: HTMLElement, size: number): Println {
let lastAnimationFrame: number | null = null; // last scheduled animation frame let lastAnimationFrame: number | null = null // last scheduled animation frame
const buffer: Array<string> = []; // the internal buffer of lines const buffer: string[] = [] // the internal buffer of lines
const paint = () => { const paint = (): void => {
println.paintedFrames++ println.paintedFrames++
target.innerText = buffer.join("\n") target.innerText = buffer.join('\n')
scrollContainer.scrollTop = scrollContainer.scrollHeight scrollContainer.scrollTop = scrollContainer.scrollHeight
lastAnimationFrame = null lastAnimationFrame = null
} }
const println = (line: string, flush?: boolean) => { const println = (line: string, flush?: boolean): void => {
// add the line // add the line
buffer.push(line) buffer.push(line)
if (size !== 0 && buffer.length > size) { if (size !== 0 && buffer.length > size) {
@ -35,39 +35,56 @@ function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size:
} }
// force a repaint! // force a repaint!
if(flush) return paint(); if (flush === true) return paint()
// schedule an animation frame // schedule an animation frame
lastAnimationFrame = window.requestAnimationFrame(paint); lastAnimationFrame = window.requestAnimationFrame(paint)
} }
println.paintedFrames = 0; println.paintedFrames = 0
println.missedFrames = 0; println.missedFrames = 0
return println; return println
} }
export default function setup() { export default function setup (): void {
const remote_action = document.getElementsByClassName('remote-action') const remoteAction = document.getElementsByClassName('remote-action')
Array.from(remote_action).forEach((element) => { Array.from(remoteAction).forEach((element) => {
const action = element.getAttribute('data-action') as string; const action = element.getAttribute('data-action') as string
const reload = element.getAttribute('data-force-reload'); const reload = element.getAttribute('data-force-reload')
const param = element.getAttribute('data-param') as string | undefined; const param = element.getAttribute('data-param') as string | undefined
const confirmElementName = element.getAttribute('data-confirm-param'); const confirmElementName = element.getAttribute('data-confirm-param')
const confirmElement = (confirmElementName ? document.querySelector(confirmElementName) : null) as HTMLInputElement | null; const confirmElement = typeof confirmElementName === 'string' ? document.querySelector(confirmElementName) : null
const bufferSize = (function () { const getConfirmValue = (): string | null => {
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0; if (confirmElement === null) {
return (isFinite(number) && number > 0) ? number : 0; console.warn('data-confirm-param was not found')
})() return null
}
const validate = function() { if (!('value' in confirmElement)) {
if (!confirmElement) return true return null
return confirmElement.value === param; }
const value = confirmElement.value
if (value === null || (typeof value !== 'string')) {
return null
} }
if (confirmElement) { return value
const runValidation = () => { }
const bufferSize = (function () {
const number = parseInt(element.getAttribute('data-buffer') ?? '', 10) ?? 0
return (isFinite(number) && number > 0) ? number : 0
})()
const validate = function (): boolean {
const confirmValue = getConfirmValue()
if (confirmValue === null) return true
return confirmValue === param
}
if (confirmElement !== null) {
const runValidation = (): void => {
if (validate()) { if (validate()) {
element.removeAttribute('disabled') element.removeAttribute('disabled')
} else { } else {
@ -78,85 +95,88 @@ export default function setup() {
runValidation() runValidation()
} }
let onClose: (success: boolean) => void | null; let onClose: ((success: boolean) => void) | undefined
if (typeof reload === 'string') { if (typeof reload === 'string') {
onClose = () => { onClose = () => {
if (reload === '') location.reload(); if (reload === '') location.reload()
else location.href = reload; else location.href = reload
} }
} }
element.addEventListener('click', function (ev) { element.addEventListener('click', function (ev) {
ev.preventDefault(); ev.preventDefault()
// do nothing if the validation fails // do nothing if the validation fails
if (!validate()) return; if (!validate()) return
// create a modal dialog // create a modal dialog
const params = (typeof param === 'string') ? [param] : []; const params = (typeof param === 'string') ? [param] : []
createModal(action, params, { createModal(action, params, {
onClose: onClose, onClose,
bufferSize: bufferSize, bufferSize
}); })
}); })
}) })
} }
interface ModalOptions {
type ModalOptions = { bufferSize: number
bufferSize: number; onClose: ((success: true) => void) & ((success: false, message: string) => void)
onClose: (success: boolean, message?: string) => void
} }
export function createModal(action: string, params: string[], opts: Partial<ModalOptions>) { export function createModal (action: string, params: string[], opts: Partial<ModalOptions>): void {
// create a modal dialog and append it to the body // create a modal dialog and append it to the body
const modal = document.createElement("div") const modal = document.createElement('div')
modal.className = "modal-terminal" modal.className = 'modal-terminal'
document.body.append(modal) document.body.append(modal)
// create a <pre> to write stuff into // create a <pre> to write stuff into
const target = document.createElement("pre") const target = document.createElement('pre')
const println = makeTextBuffer(target, modal, opts.bufferSize ?? 1000) const println = makeTextBuffer(target, modal, opts.bufferSize ?? 1000)
modal.append(target) modal.append(target)
// create a button to eventually close everything // create a button to eventually close everything
const finishButton = document.createElement("button") const finishButton = document.createElement('button')
finishButton.className = "pure-button pure-button-success" finishButton.className = 'pure-button pure-button-success'
finishButton.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close") finishButton.append(typeof opts?.onClose === 'function' ? 'Close & Finish' : 'Close')
let result: ResultMessage = {success: false}; let result: ResultMessage = { success: false, message: 'Nothing happened' }
finishButton.addEventListener('click', (event) => { finishButton.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault()
if (typeof opts?.onClose === 'function') { if (typeof opts?.onClose === 'function') {
finishButton.setAttribute('disabled', 'disabled') finishButton.setAttribute('disabled', 'disabled')
target.innerHTML = 'Finishing up ...' target.innerHTML = 'Finishing up ...'
if (result.success) {
opts.onClose(result.success)
} else {
opts.onClose(result.success, result.message) opts.onClose(result.success, result.message)
return; }
return
} }
modal.parentNode?.removeChild(modal); modal.parentNode?.removeChild(modal)
}) })
const cancelButton = document.createElement("button") const cancelButton = document.createElement('button')
cancelButton.className = "pure-button pure-button-danger" cancelButton.className = 'pure-button pure-button-danger'
cancelButton.setAttribute("disabled", "disabled") cancelButton.setAttribute('disabled', 'disabled')
cancelButton.append("Cancel") cancelButton.append('Cancel')
modal.append(cancelButton) modal.append(cancelButton)
const onbeforeunload = window.onbeforeunload; const onbeforeunload = window.onbeforeunload
window.onbeforeunload = () => "A remote session is in progress. Are you sure you want to leave?"; window.onbeforeunload = () => 'A remote session is in progress. Are you sure you want to leave?'
// when closing, add a button to the modal! // when closing, add a button to the modal!
const close = (message: ResultMessage) => { const close = (message: ResultMessage): void => {
result = message result = message
if (result.success) { if (result.success) {
println('Process completed successfully. ', true); println('Process completed successfully. ', true)
} else { } else {
println('Process reported error: ' + result.message, true); println('Process reported error: ' + result.message, true)
} }
window.onbeforeunload = onbeforeunload; window.onbeforeunload = onbeforeunload
modal.removeChild(cancelButton) modal.removeChild(cancelButton)
modal.append(finishButton) modal.append(finishButton)
@ -165,32 +185,31 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
console.debug(`Terminal: painted=${println.paintedFrames} missed=${println.missedFrames} (${quota}%)`, true) console.debug(`Terminal: painted=${println.paintedFrames} missed=${println.missedFrames} (${quota}%)`, true)
} }
println("Connecting ...", true) println('Connecting ...', true)
// connect to the socket and send the action // connect to the socket and send the action
callServerAction( callServerAction(
location.href.replace('http', 'ws'), location.href.replace('http', 'ws'),
{ {
'name': action, name: action,
'params': params, params
}, },
( (
send: (text: string) => void, send: (text: string) => void,
cancel: () => void, cancel: () => void
) => { ) => {
cancelButton.removeAttribute("disabled") cancelButton.removeAttribute('disabled')
cancelButton.addEventListener("click", (event) => { cancelButton.addEventListener('click', (event) => {
event.preventDefault() event.preventDefault()
println("Cancelling", true) println('Cancelling', true)
cancel() cancel()
}) })
println("Connected", true) println('Connected', true)
}, },
println println
).then(close) ).then(close)
.catch(() => { .catch(() => {
close({ 'success': false, 'message': "connection closed unexpectedly" }) close({ success: false, message: 'connection closed unexpectedly' })
}) })
} }

View file

@ -1,12 +1,17 @@
import { Mutex } from "async-mutex"; import { Mutex } from 'async-mutex'
export type CallMessage = { name: string; params?: string[] | null } import { runMutexExclusive } from '~/src/lib/discard'
export type ResultMessage = { success: boolean; message?: string; }
export type SignalMessage = { signal: string; } export interface CallMessage { name: string, params?: string[] | null }
function isResultMessage(value: any): value is ResultMessage { export type ResultMessage = { success: true } | { success: false, message: string }
export interface SignalMessage { signal: string }
function isResultMessage (value: any): value is ResultMessage {
return typeof value === 'object' && return typeof value === 'object' &&
Object.prototype.hasOwnProperty.call(value, 'success') && typeof value['success'] === 'boolean' && Object.prototype.hasOwnProperty.call(value, 'success') &&
(!Object.prototype.hasOwnProperty.call(value, 'message') || (value['message'] === undefined || typeof value['message'] === 'string')); (
(value.success === true) ||
(value.success === false && Object.prototype.hasOwnProperty.call(value, 'message') && (typeof value.message === 'string'))
)
} }
/** /**
@ -17,20 +22,20 @@ function isResultMessage(value: any): value is ResultMessage {
* @param onText called when the connection receives some text * @param onText called when the connection receives some text
* @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors. * @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors.
*/ */
export default async function callServerAction( export default async function callServerAction (
endpoint: string, endpoint: string,
call: CallMessage, call: CallMessage,
onOpen: (send: (text: string) => void, cancel: () => void) => void, onOpen: (send: (text: string) => void, cancel: () => void) => void,
onText: (text: string) => void, onText: (text: string) => void
): Promise<ResultMessage> { ): Promise<ResultMessage> {
return new Promise((rs, rj) => { return await new Promise((resolve, reject) => {
const mutex = new Mutex(); const mutex = new Mutex()
const socket = new WebSocket(endpoint); const socket = new WebSocket(endpoint)
let result: ResultMessage; let result: ResultMessage
socket.onmessage = (msg) => { socket.onmessage = (msg) => {
mutex.runExclusive(async () => { runMutexExclusive(mutex, async () => {
if (typeof msg.data === 'string') { if (typeof msg.data === 'string') {
onText(msg.data) onText(msg.data)
return return
@ -39,7 +44,11 @@ export default async function callServerAction(
if (msg.data instanceof Blob) { if (msg.data instanceof Blob) {
const object = JSON.parse(await msg.data.text()) const object = JSON.parse(await msg.data.text())
if (isResultMessage(object)) { if (isResultMessage(object)) {
result = {'success': object['success'], 'message': object['message']} if (object.success) {
result = { success: true }
} else {
result = { success: false, message: object.message }
}
return return
} }
} }
@ -48,12 +57,12 @@ export default async function callServerAction(
}) })
} }
socket.onclose = () => { socket.onclose = () => {
mutex.runExclusive(() => rs(result)); mutex.runExclusive(() => resolve(result)).then(() => {}).catch(console.error.bind(console))
}; }
socket.onerror = rj; // if an error occurs, close the socket socket.onerror = reject // if an error occurs, close the socket
socket.onopen = () => { socket.onopen = () => {
const blob = new Blob([JSON.stringify(call)]); const blob = new Blob([JSON.stringify(call)])
socket.send(blob) socket.send(blob)
onOpen( onOpen(
@ -65,7 +74,7 @@ export default async function callServerAction(
socket.send(text) socket.send(text)
}, },
() => { () => {
const blob = new Blob([JSON.stringify({'signal': 'cancel'})]); const blob = new Blob([JSON.stringify({ signal: 'cancel' })])
socket.send(blob) socket.send(blob)
} }
) )

View file

@ -1,41 +1,43 @@
document.querySelectorAll('span').forEach((elem: Element) => { document.querySelectorAll('span').forEach((elem: Element) => {
if (!elem.hasAttribute('data-reveal')) return if (!elem.hasAttribute('data-reveal')) return
addReveal(elem as HTMLSpanElement, 10000); addReveal(elem as HTMLSpanElement, 10000)
}) })
export function addReveal(span: HTMLSpanElement, hideDelay: number) { export function addReveal (span: HTMLSpanElement, hideDelay: number): void {
const content = span.getAttribute("data-reveal") ?? '(no content)' const content = span.getAttribute('data-reveal') ?? '(no content)'
let isHidden = true let isHidden = true
// handler to hide the element // handler to hide the element
const hide = () => { const hide = (): void => {
isHidden = true isHidden = true
span.innerText = "(click to reveal)" span.innerText = '(click to reveal)'
} }
hide() hide()
const reveal = () => { const reveal = (): void => {
isHidden = false isHidden = false
const code = document.createElement('code') const code = document.createElement('code')
code.append(content) code.append(content)
code.addEventListener('click', (evt) => { code.addEventListener('click', (evt) => {
evt.preventDefault() evt.preventDefault()
// Check if the clipboard api is supported
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!navigator.clipboard) return if (!navigator.clipboard) return
navigator.clipboard.writeText(content)
})
code.style.userSelect = "all";
span.innerHTML = "" navigator.clipboard.writeText(content).then(() => {}).catch(console.error.bind(console))
})
code.style.userSelect = 'all'
span.innerHTML = ''
span.append(code) span.append(code)
} }
span.addEventListener("click", (evt) => { span.addEventListener('click', (evt) => {
evt.preventDefault() evt.preventDefault()
if (!isHidden) return if (!isHidden) return
reveal() reveal()
setTimeout(hide, hideDelay) // hide again after 1 second setTimeout(hide, hideDelay) // hide again after 1 second

File diff suppressed because it is too large Load diff