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

View file

@ -1,196 +1,215 @@
import "./index.css"
import './index.css'
import callServerAction, { ResultMessage } from './proto'
type Println = ((line: string, flush?: boolean) => void) & {
paintedFrames: number;
missedFrames: number;
paintedFrames: number
missedFrames: number
}
/**
* 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.
*/
function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size: number): Println {
let lastAnimationFrame: number | null = null; // last scheduled animation frame
function makeTextBuffer (target: HTMLElement, scrollContainer: HTMLElement, size: number): Println {
let lastAnimationFrame: number | null = null // last scheduled animation frame
const buffer: Array<string> = []; // the internal buffer of lines
const paint = () => {
println.paintedFrames++
target.innerText = buffer.join("\n")
scrollContainer.scrollTop = scrollContainer.scrollHeight
lastAnimationFrame = null
const buffer: string[] = [] // the internal buffer of lines
const paint = (): void => {
println.paintedFrames++
target.innerText = buffer.join('\n')
scrollContainer.scrollTop = scrollContainer.scrollHeight
lastAnimationFrame = null
}
const println = (line: string, flush?: boolean): void => {
// add the line
buffer.push(line)
if (size !== 0 && buffer.length > size) {
buffer.splice(0, buffer.length - size)
}
const println = (line: string, flush?: boolean) => {
// add the line
buffer.push(line)
if (size !== 0 && buffer.length > size) {
buffer.splice(0, buffer.length - size)
}
// and update the browser in the next animation frame
if (lastAnimationFrame !== null) {
println.missedFrames++
window.cancelAnimationFrame(lastAnimationFrame)
}
// force a repaint!
if(flush) return paint();
// schedule an animation frame
lastAnimationFrame = window.requestAnimationFrame(paint);
// and update the browser in the next animation frame
if (lastAnimationFrame !== null) {
println.missedFrames++
window.cancelAnimationFrame(lastAnimationFrame)
}
println.paintedFrames = 0;
println.missedFrames = 0;
return println;
// force a repaint!
if (flush === true) return paint()
// schedule an animation frame
lastAnimationFrame = window.requestAnimationFrame(paint)
}
println.paintedFrames = 0
println.missedFrames = 0
return println
}
export default function setup() {
const remote_action = document.getElementsByClassName('remote-action')
Array.from(remote_action).forEach((element) => {
const action = element.getAttribute('data-action') as string;
const reload = element.getAttribute('data-force-reload');
const param = element.getAttribute('data-param') as string | undefined;
const confirmElementName = element.getAttribute('data-confirm-param');
const confirmElement = (confirmElementName ? document.querySelector(confirmElementName) : null) as HTMLInputElement | null;
const bufferSize = (function () {
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0;
return (isFinite(number) && number > 0) ? number : 0;
})()
const validate = function() {
if (!confirmElement) return true
return confirmElement.value === param;
}
if (confirmElement) {
const runValidation = () => {
if (validate()) {
element.removeAttribute('disabled')
} else {
element.setAttribute('disabled', 'disabled')
}
}
confirmElement.addEventListener('change', runValidation)
runValidation()
}
export default function setup (): void {
const remoteAction = document.getElementsByClassName('remote-action')
Array.from(remoteAction).forEach((element) => {
const action = element.getAttribute('data-action') as string
const reload = element.getAttribute('data-force-reload')
const param = element.getAttribute('data-param') as string | undefined
let onClose: (success: boolean) => void | null;
if (typeof reload === 'string') {
onClose = () => {
if (reload === '') location.reload();
else location.href = reload;
}
}
element.addEventListener('click', function (ev) {
ev.preventDefault();
// do nothing if the validation fails
if (!validate()) return;
const confirmElementName = element.getAttribute('data-confirm-param')
const confirmElement = typeof confirmElementName === 'string' ? document.querySelector(confirmElementName) : null
// create a modal dialog
const params = (typeof param === 'string') ? [param] : [];
createModal(action, params, {
onClose: onClose,
bufferSize: bufferSize,
});
});
})
}
const getConfirmValue = (): string | null => {
if (confirmElement === null) {
console.warn('data-confirm-param was not found')
return null
}
if (!('value' in confirmElement)) {
return null
}
const value = confirmElement.value
if (value === null || (typeof value !== 'string')) {
return null
}
return value
}
type ModalOptions = {
bufferSize: number;
onClose: (success: boolean, message?: string) => void
}
export function createModal(action: string, params: string[], opts: Partial<ModalOptions>) {
// create a modal dialog and append it to the body
const modal = document.createElement("div")
modal.className = "modal-terminal"
document.body.append(modal)
const bufferSize = (function () {
const number = parseInt(element.getAttribute('data-buffer') ?? '', 10) ?? 0
return (isFinite(number) && number > 0) ? number : 0
})()
// create a <pre> to write stuff into
const target = document.createElement("pre")
const println = makeTextBuffer(target, modal, opts.bufferSize ?? 1000)
modal.append(target)
// create a button to eventually close everything
const finishButton = document.createElement("button")
finishButton.className = "pure-button pure-button-success"
finishButton.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close")
const validate = function (): boolean {
const confirmValue = getConfirmValue()
if (confirmValue === null) return true
return confirmValue === param
}
let result: ResultMessage = {success: false};
finishButton.addEventListener('click', (event) => {
event.preventDefault();
if (typeof opts?.onClose === 'function') {
finishButton.setAttribute('disabled', 'disabled')
target.innerHTML = 'Finishing up ...'
opts.onClose(result.success, result.message)
return;
}
modal.parentNode?.removeChild(modal);
})
const cancelButton = document.createElement("button")
cancelButton.className = "pure-button pure-button-danger"
cancelButton.setAttribute("disabled", "disabled")
cancelButton.append("Cancel")
modal.append(cancelButton)
const onbeforeunload = window.onbeforeunload;
window.onbeforeunload = () => "A remote session is in progress. Are you sure you want to leave?";
// when closing, add a button to the modal!
const close = (message: ResultMessage) => {
result = message
if (result.success) {
println('Process completed successfully. ', true);
if (confirmElement !== null) {
const runValidation = (): void => {
if (validate()) {
element.removeAttribute('disabled')
} else {
println('Process reported error: ' + result.message, true);
element.setAttribute('disabled', 'disabled')
}
window.onbeforeunload = onbeforeunload;
modal.removeChild(cancelButton)
modal.append(finishButton)
const quota = (println.paintedFrames / (println.missedFrames + println.paintedFrames)) * 100
console.debug(`Terminal: painted=${println.paintedFrames} missed=${println.missedFrames} (${quota}%)`, true)
}
confirmElement.addEventListener('change', runValidation)
runValidation()
}
println("Connecting ...", true)
let onClose: ((success: boolean) => void) | undefined
if (typeof reload === 'string') {
onClose = () => {
if (reload === '') location.reload()
else location.href = reload
}
}
element.addEventListener('click', function (ev) {
ev.preventDefault()
// connect to the socket and send the action
callServerAction(
location.href.replace('http', 'ws'),
{
'name': action,
'params': params,
},
(
send: (text: string) => void,
cancel: () => void,
) => {
cancelButton.removeAttribute("disabled")
cancelButton.addEventListener("click", (event) => {
event.preventDefault()
// do nothing if the validation fails
if (!validate()) return
println("Cancelling", true)
cancel()
})
println("Connected", true)
},
println
).then(close)
.catch(() => {
close({ 'success': false, 'message': "connection closed unexpectedly" })
// create a modal dialog
const params = (typeof param === 'string') ? [param] : []
createModal(action, params, {
onClose,
bufferSize
})
})
}
})
}
interface ModalOptions {
bufferSize: number
onClose: ((success: true) => void) & ((success: false, message: string) => void)
}
export function createModal (action: string, params: string[], opts: Partial<ModalOptions>): void {
// create a modal dialog and append it to the body
const modal = document.createElement('div')
modal.className = 'modal-terminal'
document.body.append(modal)
// create a <pre> to write stuff into
const target = document.createElement('pre')
const println = makeTextBuffer(target, modal, opts.bufferSize ?? 1000)
modal.append(target)
// create a button to eventually close everything
const finishButton = document.createElement('button')
finishButton.className = 'pure-button pure-button-success'
finishButton.append(typeof opts?.onClose === 'function' ? 'Close & Finish' : 'Close')
let result: ResultMessage = { success: false, message: 'Nothing happened' }
finishButton.addEventListener('click', (event) => {
event.preventDefault()
if (typeof opts?.onClose === 'function') {
finishButton.setAttribute('disabled', 'disabled')
target.innerHTML = 'Finishing up ...'
if (result.success) {
opts.onClose(result.success)
} else {
opts.onClose(result.success, result.message)
}
return
}
modal.parentNode?.removeChild(modal)
})
const cancelButton = document.createElement('button')
cancelButton.className = 'pure-button pure-button-danger'
cancelButton.setAttribute('disabled', 'disabled')
cancelButton.append('Cancel')
modal.append(cancelButton)
const onbeforeunload = window.onbeforeunload
window.onbeforeunload = () => 'A remote session is in progress. Are you sure you want to leave?'
// when closing, add a button to the modal!
const close = (message: ResultMessage): void => {
result = message
if (result.success) {
println('Process completed successfully. ', true)
} else {
println('Process reported error: ' + result.message, true)
}
window.onbeforeunload = onbeforeunload
modal.removeChild(cancelButton)
modal.append(finishButton)
const quota = (println.paintedFrames / (println.missedFrames + println.paintedFrames)) * 100
console.debug(`Terminal: painted=${println.paintedFrames} missed=${println.missedFrames} (${quota}%)`, true)
}
println('Connecting ...', true)
// connect to the socket and send the action
callServerAction(
location.href.replace('http', 'ws'),
{
name: action,
params
},
(
send: (text: string) => void,
cancel: () => void
) => {
cancelButton.removeAttribute('disabled')
cancelButton.addEventListener('click', (event) => {
event.preventDefault()
println('Cancelling', true)
cancel()
})
println('Connected', true)
},
println
).then(close)
.catch(() => {
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 }
export type ResultMessage = { success: boolean; message?: string; }
export type SignalMessage = { signal: string; }
function isResultMessage(value: any): value is ResultMessage {
return typeof value === 'object' &&
Object.prototype.hasOwnProperty.call(value, 'success') && typeof value['success'] === 'boolean' &&
(!Object.prototype.hasOwnProperty.call(value, 'message') || (value['message'] === undefined || typeof value['message'] === 'string'));
import { runMutexExclusive } from '~/src/lib/discard'
export interface CallMessage { name: string, params?: string[] | null }
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' &&
Object.prototype.hasOwnProperty.call(value, 'success') &&
(
(value.success === true) ||
(value.success === false && Object.prototype.hasOwnProperty.call(value, 'message') && (typeof value.message === 'string'))
)
}
/**
@ -17,58 +22,62 @@ function isResultMessage(value: any): value is ResultMessage {
* @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.
*/
export default async function callServerAction(
endpoint: string,
call: CallMessage,
onOpen: (send: (text: string) => void, cancel: () => void) => void,
onText: (text: string) => void,
export default async function callServerAction (
endpoint: string,
call: CallMessage,
onOpen: (send: (text: string) => void, cancel: () => void) => void,
onText: (text: string) => void
): Promise<ResultMessage> {
return new Promise((rs, rj) => {
const mutex = new Mutex();
return await new Promise((resolve, reject) => {
const mutex = new Mutex()
const socket = new WebSocket(endpoint);
const socket = new WebSocket(endpoint)
let result: ResultMessage;
socket.onmessage = (msg) => {
mutex.runExclusive(async () => {
if (typeof msg.data === 'string') {
onText(msg.data)
return
}
if (msg.data instanceof Blob) {
const object = JSON.parse(await msg.data.text())
if (isResultMessage(object)) {
result = {'success': object['success'], 'message': object['message']}
return
}
}
console.warn('Unknown message', msg)
})
let result: ResultMessage
socket.onmessage = (msg) => {
runMutexExclusive(mutex, async () => {
if (typeof msg.data === 'string') {
onText(msg.data)
return
}
socket.onclose = () => {
mutex.runExclusive(() => rs(result));
};
socket.onerror = rj; // if an error occurs, close the socket
socket.onopen = () => {
const blob = new Blob([JSON.stringify(call)]);
socket.send(blob)
onOpen(
(text: string) => {
if (typeof text !== 'string') {
console.warn('Ignoring send() call with unknown type', text)
return
}
socket.send(text)
},
() => {
const blob = new Blob([JSON.stringify({'signal': 'cancel'})]);
socket.send(blob)
}
)
if (msg.data instanceof Blob) {
const object = JSON.parse(await msg.data.text())
if (isResultMessage(object)) {
if (object.success) {
result = { success: true }
} else {
result = { success: false, message: object.message }
}
return
}
}
})
}
console.warn('Unknown message', msg)
})
}
socket.onclose = () => {
mutex.runExclusive(() => resolve(result)).then(() => {}).catch(console.error.bind(console))
}
socket.onerror = reject // if an error occurs, close the socket
socket.onopen = () => {
const blob = new Blob([JSON.stringify(call)])
socket.send(blob)
onOpen(
(text: string) => {
if (typeof text !== 'string') {
console.warn('Ignoring send() call with unknown type', text)
return
}
socket.send(text)
},
() => {
const blob = new Blob([JSON.stringify({ signal: 'cancel' })])
socket.send(blob)
}
)
}
})
}