chore: OliveTin 3k progress
|
|
@ -4,11 +4,12 @@ service/OliveTin
|
||||||
service/OliveTin.armhf
|
service/OliveTin.armhf
|
||||||
service/OliveTin.exe
|
service/OliveTin.exe
|
||||||
service/reports
|
service/reports
|
||||||
|
service/gen
|
||||||
releases/
|
releases/
|
||||||
dist/
|
dist/
|
||||||
installation-id.txt
|
installation-id.txt
|
||||||
tmp/
|
tmp/
|
||||||
webui/
|
frontend/dist/
|
||||||
webui.dev/node_modules
|
frontend/node_modules
|
||||||
webui.dev/.parcel-cache
|
custom-frontend
|
||||||
custom-webui
|
integration-tests/screenshots/
|
||||||
|
|
|
||||||
10
Makefile
|
|
@ -47,16 +47,8 @@ devrun: compile
|
||||||
|
|
||||||
devcontainer: compile podman-image podman-container
|
devcontainer: compile podman-image podman-container
|
||||||
|
|
||||||
webui-codestyle:
|
|
||||||
$(MAKE) -wC webui.dev codestyle
|
|
||||||
|
|
||||||
webui-dist:
|
webui-dist:
|
||||||
$(call delete-files,webui)
|
$(MAKE) -wC frontend dist
|
||||||
$(call delete-files,webui.dev/dist)
|
|
||||||
cd webui.dev && npm install
|
|
||||||
cd webui.dev && npx parcel build --public-url "."
|
|
||||||
python -c "import shutil;shutil.move('webui.dev/dist', 'webui')"
|
|
||||||
python -c "import shutil;import glob;[shutil.copy(f, 'webui') for f in glob.glob('webui.dev/*.png')]"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(call delete-files,dist)
|
$(call delete-files,dist)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ logLevel: "INFO"
|
||||||
# Checking for updates https://docs.olivetin.app/reference/updateChecks.html
|
# Checking for updates https://docs.olivetin.app/reference/updateChecks.html
|
||||||
checkForUpdates: false
|
checkForUpdates: false
|
||||||
|
|
||||||
|
authLocalUsers:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# Actions are commands that are executed by OliveTin, and normally show up as
|
# Actions are commands that are executed by OliveTin, and normally show up as
|
||||||
# buttons on the WebUI.
|
# buttons on the WebUI.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
fund=false
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
define delete-files
|
||||||
|
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||||
|
endef
|
||||||
|
|
||||||
|
codestyle:
|
||||||
|
npm install
|
||||||
|
npx eslint --fix main.js js/*
|
||||||
|
npx stylelint style.css
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(call delete-files,dist)
|
||||||
|
|
||||||
|
deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build:
|
||||||
|
npx vite build
|
||||||
|
|
||||||
|
dist: deps clean build
|
||||||
|
|
||||||
|
.PHONY: codestyle
|
||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
|
@ -22,79 +22,11 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<slot id = "app" />
|
||||||
<button id = "sidebar-toggler-button" aria-label = "Open sidebar navigation" aria-pressed = "false" aria-haspopup = "menu">☰</button>
|
|
||||||
|
|
||||||
<h1 id = "page-title">OliveTin</h1>
|
|
||||||
|
|
||||||
<nav id = "mainnav" hidden>
|
|
||||||
<ul id = "navigation-links">
|
|
||||||
<li title = "Actions">
|
|
||||||
<a id = "showActions">Actions</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul id = "supplemental-links">
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class = "userinfo">
|
|
||||||
<span id = "link-login" hidden><a href = "/login">Login</a> |</span>
|
|
||||||
<span id = "link-logout" hidden><a href = "/api/Logout">Logout</a> |</span>
|
|
||||||
<span id = "username"> </span>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984"/></g></svg>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main title = "main content">
|
<main title = "main content">
|
||||||
<section id = "contentLogs" title = "Logs" class = "box-shadow" hidden>
|
|
||||||
<div class = "toolbar">
|
|
||||||
<label class = "input-with-icons">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"/></svg>
|
|
||||||
<input placeholder = "Search for action name" id = "logSearchBox" />
|
|
||||||
<button id = "searchLogsClear" title = "Clear search filter" disabled>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/></svg>
|
|
||||||
</button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<table id = "logsTable" title = "Logs" hidden>
|
|
||||||
<thead>
|
|
||||||
<tr title = "untitled">
|
|
||||||
<th>Timestamp</th>
|
|
||||||
<th>Action</th>
|
|
||||||
<th>Metadata</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id = "logTableBody" />
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p id = "logsTableEmpty">There are no logs to display. <a href = "/">Return to index</a></p>
|
<section id = "contentActions" title = "Actions" class = "transparent" hidden >
|
||||||
|
|
||||||
<p><strong>Note:</strong> The server is configured to only send <strong id = "logs-server-page-size">?</strong> log entries at a time. The search box at the top of this page only searches this current page of logs.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id = "contentDiagnostics" title = "Diagnostics" class = "box-shadow" hidden>
|
|
||||||
<div id = "diagnostics" class = "ta-left">
|
|
||||||
<p><strong>Note:</strong> Diagnostics are only generated on OliveTin startup - they are not updated in real-time or when you refresh this page. They are intended as a "quick reference" to help you.</p>
|
|
||||||
<p>If you are having problems with OliveTin and want to raise a support request, please don't take a screenshot or copy text from this page, but instead it is highly recommended to include a <a href = "https://docs.olivetin.app/sosreport.html">sosreport</a> which is more detailed, and makes it easier to help you.</p>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<th colspan = "99">SSH</th>
|
|
||||||
<tr>
|
|
||||||
<td width = "10%">Found Key</td>
|
|
||||||
<td id = "diagnostics-sshfoundkey">?</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>Found Config</td>
|
|
||||||
<td id = "diagnostics-sshfoundconfig">?</td>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id = "contentActions" title = "Actions" hidden >
|
|
||||||
<fieldset id = "root-group" title = "Actions">
|
<fieldset id = "root-group" title = "Actions">
|
||||||
<legend hidden>Actions</legend>
|
<legend hidden>Actions</legend>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -105,22 +37,6 @@
|
||||||
</noscript>
|
</noscript>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer title = "footer">
|
|
||||||
<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
|
|
||||||
<p>
|
|
||||||
<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
|
|
||||||
<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
|
|
||||||
<span>Version: <span id = "currentVersion">?</span></span> |
|
|
||||||
<span>Server connection:
|
|
||||||
<span id = "serverConnectionRest">REST</span>,
|
|
||||||
<span id = "serverConnectionWebSocket">WebSocket</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<dialog title = "Big Error Message" id = "big-error" class = "error padded-content">
|
<dialog title = "Big Error Message" id = "big-error" class = "error padded-content">
|
||||||
|
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
@ -175,16 +91,16 @@
|
||||||
<h2>Local Login</h2>
|
<h2>Local Login</h2>
|
||||||
<div class = "error"></div>
|
<div class = "error"></div>
|
||||||
<div class = "arguments">
|
<div class = "arguments">
|
||||||
<label for = "username">
|
<label for = "in-username">
|
||||||
<span>Username:</span>
|
<span>Username:</span>
|
||||||
</label>
|
</label>
|
||||||
<input type = "text" name = "username" class = "username" />
|
<input type = "text" name = "username" id = "in-username" class = "username" autocomplete = "username"/>
|
||||||
<span></span>
|
<span></span>
|
||||||
|
|
||||||
<label for = "password">
|
<label for = "in-password">
|
||||||
<span>Password:</span>
|
<span>Password:</span>
|
||||||
</label>
|
</label>
|
||||||
<input type = "password" name = "password" class = "password" />
|
<input type = "password" name = "password" id = "in-password" class = "password" />
|
||||||
<span></span>
|
<span></span>
|
||||||
|
|
||||||
<button type = "submit">Login</button>
|
<button type = "submit">Login</button>
|
||||||
|
|
@ -245,6 +161,9 @@
|
||||||
to at least display a helpful error message if we can't use OliveTin.
|
to at least display a helpful error message if we can't use OliveTin.
|
||||||
*/
|
*/
|
||||||
window.showBigError = function (type, friendlyType, message, isFatal) {
|
window.showBigError = function (type, friendlyType, message, isFatal) {
|
||||||
|
console.error('Error ' + type + ': ', message)
|
||||||
|
return;
|
||||||
|
|
||||||
bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
|
bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
|
||||||
|
|
||||||
if (isFatal) {
|
if (isFatal) {
|
||||||
|
|
@ -180,30 +180,7 @@ class ArgumentForm extends window.HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
domEl.onchange = () => {
|
domEl.onchange = () => {
|
||||||
const validateArgumentTypeArgs = {
|
formatValidation(domEl, arg)
|
||||||
value: domEl.value,
|
|
||||||
type: arg.type
|
|
||||||
}
|
|
||||||
|
|
||||||
window.fetch(window.restBaseUrl + 'ValidateArgumentType', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(validateArgumentTypeArgs)
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json()
|
|
||||||
} else {
|
|
||||||
throw new Error(res.statusText)
|
|
||||||
}
|
|
||||||
}).then((json) => {
|
|
||||||
if (json.valid) {
|
|
||||||
domEl.setCustomValidity('')
|
|
||||||
} else {
|
|
||||||
domEl.setCustomValidity(json.description)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -232,6 +209,21 @@ class ArgumentForm extends window.HTMLElement {
|
||||||
return domEl
|
return domEl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async formatValidation (domEl, arg) {
|
||||||
|
const validateArgumentTypeArgs = {
|
||||||
|
value: domEl.value,
|
||||||
|
type: arg.type
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await window.validateArgumentType(validateArgumentTypeArgs)
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
domEl.setCustomValidity('')
|
||||||
|
} else {
|
||||||
|
domEl.setCustomValidity(validation.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateUrlWithArg (ev) {
|
updateUrlWithArg (ev) {
|
||||||
if (!ev.target.name) {
|
if (!ev.target.name) {
|
||||||
return
|
return
|
||||||
|
|
@ -37,9 +37,12 @@ export class OutputTerminal {
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
unlock()
|
unlock()
|
||||||
|
|
||||||
|
if (then != null && then !== undefined) {
|
||||||
then()
|
then()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async reset () {
|
async reset () {
|
||||||
const unlock = await this.writeMutex.lock()
|
const unlock = await this.writeMutex.lock()
|
||||||
|
|
@ -1,12 +1,3 @@
|
||||||
import './ActionButton.js' // To define action-button
|
|
||||||
import { NavigationBar } from './NavigationBar.js'
|
|
||||||
import { ExecutionDialog } from './ExecutionDialog.js'
|
|
||||||
import { ActionStatusDisplay } from './ActionStatusDisplay.js'
|
|
||||||
|
|
||||||
function getQueryParams () {
|
|
||||||
return new URLSearchParams(window.location.search.substring(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAndTriggerActionFromQueryParam () {
|
function checkAndTriggerActionFromQueryParam () {
|
||||||
const params = getQueryParams()
|
const params = getQueryParams()
|
||||||
|
|
||||||
|
|
@ -77,18 +68,8 @@ function createAnnotation (key, val) {
|
||||||
* This is a weird function that just sets some globals.
|
* This is a weird function that just sets some globals.
|
||||||
*/
|
*/
|
||||||
export function initMarshaller () {
|
export function initMarshaller () {
|
||||||
window.navbar = new NavigationBar()
|
|
||||||
|
|
||||||
window.showSection = showSection
|
|
||||||
window.showSectionView = showSectionView
|
|
||||||
|
|
||||||
window.executionDialog = new ExecutionDialog()
|
|
||||||
|
|
||||||
window.logEntries = new Map()
|
window.logEntries = new Map()
|
||||||
window.registeredPaths = new Map()
|
|
||||||
window.breadcrumbNavigation = []
|
|
||||||
|
|
||||||
window.currentPath = ''
|
|
||||||
|
|
||||||
window.addEventListener('EventExecutionStarted', onExecutionStarted)
|
window.addEventListener('EventExecutionStarted', onExecutionStarted)
|
||||||
window.addEventListener('EventExecutionFinished', onExecutionFinished)
|
window.addEventListener('EventExecutionFinished', onExecutionFinished)
|
||||||
|
|
@ -125,49 +106,16 @@ export function marshalDashboardComponentsJsonToHtml (json) {
|
||||||
} else {
|
} else {
|
||||||
setUsername(json.authenticatedUser, json.authenticatedUserProvider)
|
setUsername(json.authenticatedUser, json.authenticatedUserProvider)
|
||||||
|
|
||||||
marshalActionsJsonToHtml(json)
|
|
||||||
marshalDashboardStructureToHtml(json)
|
marshalDashboardStructureToHtml(json)
|
||||||
|
|
||||||
window.navbar.refreshSectionPolicyLinks(json.effectivePolicy)
|
|
||||||
|
|
||||||
refreshDiagnostics(json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.setAttribute('initial-marshal-complete', 'true')
|
document.body.setAttribute('initial-marshal-complete', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
function marshalActionsJsonToHtml (json) {
|
|
||||||
const currentIterationTimestamp = Date.now()
|
|
||||||
|
|
||||||
window.actionButtons = {}
|
|
||||||
window.actionButtonsJson = {} // Store the JSON representation
|
|
||||||
|
|
||||||
for (const jsonButton of json.actions) {
|
|
||||||
let htmlButton = window.actionButtons[jsonButton.id]
|
|
||||||
|
|
||||||
if (typeof htmlButton === 'undefined') {
|
|
||||||
htmlButton = document.createElement('action-button')
|
|
||||||
htmlButton.constructFromJson(jsonButton)
|
|
||||||
|
|
||||||
window.actionButtons[jsonButton.title] = htmlButton
|
|
||||||
window.actionButtonsJson[jsonButton.title] = jsonButton // Store the JSON representation
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlButton.updateFromJson(jsonButton)
|
|
||||||
htmlButton.updateIterationTimestamp = currentIterationTimestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove existing, but stale buttons (that were not updated in this round)
|
|
||||||
for (const existingButton of document.querySelectorAll('action-button')) {
|
|
||||||
if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
|
|
||||||
existingButton.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOutputChunk (evt) {
|
function onOutputChunk (evt) {
|
||||||
const chunk = evt.payload
|
const chunk = evt.payload
|
||||||
|
|
||||||
|
return;
|
||||||
if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) {
|
if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) {
|
||||||
window.terminal.write(chunk.output)
|
window.terminal.write(chunk.output)
|
||||||
}
|
}
|
||||||
|
|
@ -176,9 +124,9 @@ function onOutputChunk (evt) {
|
||||||
function onExecutionStarted (evt) {
|
function onExecutionStarted (evt) {
|
||||||
const logEntry = evt.payload.logEntry
|
const logEntry = evt.payload.logEntry
|
||||||
|
|
||||||
marshalLogsJsonToHtml({
|
// marshalLogsJsonToHtml({
|
||||||
logs: [logEntry]
|
// logs: [logEntry]
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecutionFinished (evt) {
|
function onExecutionFinished (evt) {
|
||||||
|
|
@ -186,11 +134,7 @@ function onExecutionFinished (evt) {
|
||||||
|
|
||||||
window.logEntries.set(logEntry.executionTrackingId, logEntry)
|
window.logEntries.set(logEntry.executionTrackingId, logEntry)
|
||||||
|
|
||||||
const actionButton = window.actionButtons[logEntry.actionTitle]
|
return;
|
||||||
|
|
||||||
if (actionButton === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionButton = document.querySelector('execution-button#execution-' + logEntry.executionTrackingId)
|
const executionButton = document.querySelector('execution-button#execution-' + logEntry.executionTrackingId)
|
||||||
let feedbackButton = actionButton
|
let feedbackButton = actionButton
|
||||||
|
|
@ -216,9 +160,9 @@ function onExecutionFinished (evt) {
|
||||||
|
|
||||||
feedbackButton.onExecutionFinished(logEntry)
|
feedbackButton.onExecutionFinished(logEntry)
|
||||||
|
|
||||||
marshalLogsJsonToHtml({
|
// marshalLogsJsonToHtml({
|
||||||
logs: [logEntry]
|
// logs: [logEntry]
|
||||||
})
|
// })
|
||||||
|
|
||||||
// If the current execution dialog is open, update that too
|
// If the current execution dialog is open, update that too
|
||||||
if (window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
|
if (window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
|
||||||
|
|
@ -251,54 +195,6 @@ function showExecutionResult (pathName) {
|
||||||
window.executionDialog.show()
|
window.executionDialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSection (pathName) {
|
|
||||||
if (pathName.startsWith('/logs/')) {
|
|
||||||
showExecutionResult(pathName)
|
|
||||||
pushNewNavigationPath(pathName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = window.registeredPaths.get(pathName)
|
|
||||||
|
|
||||||
if (path === undefined) {
|
|
||||||
console.warn('Section not found by path: ' + pathName)
|
|
||||||
|
|
||||||
showSection('/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
window.convertPathToBreadcrumb = convertPathToBreadcrumb
|
|
||||||
window.currentPath = pathName
|
|
||||||
window.breadcrumbNavigation = convertPathToBreadcrumb(pathName)
|
|
||||||
|
|
||||||
for (const section of document.querySelectorAll('section')) {
|
|
||||||
if (section.title === path.section) {
|
|
||||||
section.style.display = 'block'
|
|
||||||
} else {
|
|
||||||
section.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for action parameter in query string
|
|
||||||
if (!checkAndTriggerActionFromQueryParam()) {
|
|
||||||
pushNewNavigationPath(pathName)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSectionNavigationVisible(false)
|
|
||||||
|
|
||||||
showSectionView(path.view)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushNewNavigationPath (pathName) {
|
|
||||||
// Get the current query string
|
|
||||||
const queryString = window.location.search
|
|
||||||
|
|
||||||
// Push the new state with the path and preserve the query string
|
|
||||||
window.history.pushState({
|
|
||||||
path: pathName
|
|
||||||
}, null, pathName + queryString)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSectionNavigationVisible (visible) {
|
function setSectionNavigationVisible (visible) {
|
||||||
const nav = document.querySelector('nav')
|
const nav = document.querySelector('nav')
|
||||||
const btn = document.getElementById('sidebar-toggler-button')
|
const btn = document.getElementById('sidebar-toggler-button')
|
||||||
|
|
@ -345,41 +241,6 @@ export function setupSectionNavigation (style) {
|
||||||
|
|
||||||
document.body.classList.add('has-topbar')
|
document.body.classList.add('has-topbar')
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSection('/', 'Actions', null, document.getElementById('showActions'))
|
|
||||||
registerSection('/diagnostics', 'Diagnostics', null, null)
|
|
||||||
registerSection('/logs', 'Logs', null, null)
|
|
||||||
registerSection('/login', 'Login', null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerSection (path, section, view, linkElement) {
|
|
||||||
window.registeredPaths.set(path, {
|
|
||||||
section: section,
|
|
||||||
view: view
|
|
||||||
})
|
|
||||||
|
|
||||||
if (linkElement != null) {
|
|
||||||
addLinkToSection(path, linkElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLinkToSection (pathName, element) {
|
|
||||||
const path = window.registeredPaths.get(pathName)
|
|
||||||
|
|
||||||
element.href = 'javascript:void(0)'
|
|
||||||
element.title = path.section
|
|
||||||
element.onclick = () => {
|
|
||||||
showSection(pathName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshDiagnostics (json) {
|
|
||||||
document.getElementById('diagnostics-sshfoundkey').innerHTML = json.diagnostics.SshFoundKey
|
|
||||||
document.getElementById('diagnostics-sshfoundconfig').innerHTML = json.diagnostics.SshFoundConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemTitle (title) {
|
|
||||||
return title.replaceAll(' ', '')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function marshalSingleDashboard (dashboard, nav) {
|
function marshalSingleDashboard (dashboard, nav) {
|
||||||
|
|
@ -409,10 +270,10 @@ function marshalSingleDashboard (dashboard, nav) {
|
||||||
|
|
||||||
window.navbar.createLink(dashboard.title, systemTitleUrl, false)
|
window.navbar.createLink(dashboard.title, systemTitleUrl, false)
|
||||||
|
|
||||||
registerSection(systemTitleUrl, section.title, null, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function marshalDashboardStructureToHtml (json) {
|
function marshalDashboardStructureToHtml (json) {
|
||||||
|
return
|
||||||
const nav = document.getElementById('navigation-links')
|
const nav = document.getElementById('navigation-links')
|
||||||
|
|
||||||
for (const dashboard of json.dashboards) {
|
for (const dashboard of json.dashboards) {
|
||||||
|
|
@ -447,6 +308,7 @@ function marshalDashboardStructureToHtml (json) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function marshalLink (item, fieldset) {
|
function marshalLink (item, fieldset) {
|
||||||
|
return
|
||||||
let btn = window.actionButtons[item.title]
|
let btn = window.actionButtons[item.title]
|
||||||
|
|
||||||
if (typeof btn === 'undefined') {
|
if (typeof btn === 'undefined') {
|
||||||
|
|
@ -467,27 +329,19 @@ function marshalMreOutput (dashboardComponent, fieldset) {
|
||||||
pre.classList.add('mre-output')
|
pre.classList.add('mre-output')
|
||||||
pre.innerHTML = 'Waiting...'
|
pre.innerHTML = 'Waiting...'
|
||||||
|
|
||||||
const executionStatus = {
|
const args = {
|
||||||
actionId: dashboardComponent.title
|
actionId: dashboardComponent.title
|
||||||
}
|
}
|
||||||
|
|
||||||
window.fetch(window.restBaseUrl + 'ExecutionStatus', {
|
try {
|
||||||
method: 'POST',
|
const status = window.client.executionStatus(args)
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
updateMre(pre, status.logEntry)
|
||||||
},
|
} catch (err) {
|
||||||
body: JSON.stringify(executionStatus)
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json()
|
|
||||||
} else {
|
|
||||||
pre.innerHTML = 'error'
|
pre.innerHTML = 'error'
|
||||||
|
|
||||||
throw new Error(res.statusText)
|
throw new Error(res.statusText)
|
||||||
}
|
}
|
||||||
}).then((json) => {
|
|
||||||
updateMre(pre, json.logEntry)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateMre = (pre, json) => {
|
const updateMre = (pre, json) => {
|
||||||
pre.innerHTML = json.output
|
pre.innerHTML = json.output
|
||||||
|
|
@ -556,68 +410,6 @@ function marshalFieldset (item, section, parentDashboard) {
|
||||||
section.appendChild(fs)
|
section.appendChild(fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSectionView (selected) {
|
|
||||||
if (selected === '') {
|
|
||||||
selected = null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fieldset of document.querySelectorAll('fieldset')) {
|
|
||||||
if (selected === null) {
|
|
||||||
if ((fieldset.id === 'root-group' || fieldset.getAttribute('parent-dashboard') !== '') && fieldset.children.length > 1) {
|
|
||||||
fieldset.style.display = 'grid'
|
|
||||||
} else {
|
|
||||||
fieldset.style.display = 'none'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fieldset.title === selected) {
|
|
||||||
fieldset.style.display = 'grid'
|
|
||||||
} else {
|
|
||||||
fieldset.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = window.registeredPaths.get(window.currentPath)
|
|
||||||
|
|
||||||
for (const navLink of document.querySelector('nav').querySelectorAll('a')) {
|
|
||||||
if (navLink.title === current.section) {
|
|
||||||
navLink.classList.add('selected')
|
|
||||||
} else {
|
|
||||||
navLink.classList.remove('selected')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuildH1BreadcrumbNavigation(selected)
|
|
||||||
|
|
||||||
pushNewNavigationPath(window.currentPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildH1BreadcrumbNavigation () {
|
|
||||||
const title = document.querySelector('h1')
|
|
||||||
title.innerHTML = ''
|
|
||||||
|
|
||||||
const rootLink = document.createElement('a')
|
|
||||||
rootLink.innerText = window.pageTitle
|
|
||||||
rootLink.href = 'javascript:void(0)'
|
|
||||||
rootLink.onclick = () => {
|
|
||||||
showSection('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
title.appendChild(rootLink)
|
|
||||||
|
|
||||||
for (const pathName of window.breadcrumbNavigation) {
|
|
||||||
const sep = document.createElement('span')
|
|
||||||
sep.innerHTML = ' » '
|
|
||||||
title.append(sep)
|
|
||||||
|
|
||||||
const path = window.registeredPaths.get(pathName)
|
|
||||||
|
|
||||||
title.appendChild(createNavigationBreadcrumbDisplay(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
document.title = title.innerText
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNavigationBreadcrumbDisplay (path) {
|
function createNavigationBreadcrumbDisplay (path) {
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = 'javascript:void(0)'
|
a.href = 'javascript:void(0)'
|
||||||
|
|
@ -678,76 +470,9 @@ function marshalDirectory (item, section) {
|
||||||
|
|
||||||
const path = '/' + section.title + '/' + getSystemTitle(item.title)
|
const path = '/' + section.title + '/' + getSystemTitle(item.title)
|
||||||
|
|
||||||
registerSection(path, section.title, item.title, null)
|
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
export function marshalLogsJsonToHtml (json) {
|
|
||||||
// This function is called internally with a "fake" server response, that does
|
|
||||||
// not have pageSize set. So we need to check if it's set before trying to use it.
|
|
||||||
if (json.pageSize !== undefined) {
|
|
||||||
document.getElementById('logs-server-page-size').innerText = json.pageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.logs != null && json.logs.length > 0) {
|
|
||||||
document.getElementById('logsTable').hidden = false
|
|
||||||
document.getElementById('logsTableEmpty').hidden = true
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const logEntry of json.logs) {
|
|
||||||
let row = document.getElementById('log-' + logEntry.executionTrackingId)
|
|
||||||
|
|
||||||
if (row == null) {
|
|
||||||
const tpl = document.getElementById('tplLogRow')
|
|
||||||
row = tpl.content.querySelector('tr').cloneNode(true)
|
|
||||||
row.id = 'log-' + logEntry.executionTrackingId
|
|
||||||
|
|
||||||
row.querySelector('.content').onclick = () => {
|
|
||||||
window.executionDialog.reset()
|
|
||||||
window.executionDialog.show()
|
|
||||||
window.executionDialog.renderExecutionResult({
|
|
||||||
logEntry: window.logEntries.get(logEntry.executionTrackingId)
|
|
||||||
})
|
|
||||||
pushNewNavigationPath('/logs/' + logEntry.executionTrackingId)
|
|
||||||
}
|
|
||||||
|
|
||||||
row.exitCodeDisplay = new ActionStatusDisplay(row.querySelector('.exit-code'))
|
|
||||||
|
|
||||||
logEntry.dom = row
|
|
||||||
|
|
||||||
window.logEntries.set(logEntry.executionTrackingId, logEntry)
|
|
||||||
|
|
||||||
document.querySelector('#logTableBody').prepend(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
|
|
||||||
row.querySelector('.content').innerText = logEntry.actionTitle
|
|
||||||
row.querySelector('.icon').innerHTML = logEntry.actionIcon
|
|
||||||
row.setAttribute('title', logEntry.actionTitle)
|
|
||||||
|
|
||||||
row.exitCodeDisplay.update(logEntry)
|
|
||||||
|
|
||||||
row.querySelector('.tags').innerHTML = ''
|
|
||||||
|
|
||||||
for (const tag of logEntry.tags) {
|
|
||||||
row.querySelector('.tags').append(createTag(tag))
|
|
||||||
}
|
|
||||||
|
|
||||||
row.querySelector('.tags').append(createAnnotation('user', logEntry.user))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('popstate', (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (e.state != null && typeof e.state.path !== 'undefined') {
|
|
||||||
showSection(e.state.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export function refreshServerConnectionLabel () {
|
export function refreshServerConnectionLabel () {
|
||||||
if (window.restAvailable) {
|
if (window.restAvailable) {
|
||||||
document.querySelector('#serverConnectionRest').classList.remove('error')
|
document.querySelector('#serverConnectionRest').classList.remove('error')
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {
|
||||||
|
refreshServerConnectionLabel
|
||||||
|
} from './marshaller.js'
|
||||||
|
|
||||||
|
export function checkWebsocketConnection () {
|
||||||
|
reconnectWebsocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.websocketAvailable = false
|
||||||
|
|
||||||
|
async function reconnectWebsocket () {
|
||||||
|
if (window.websocketAvailable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.websocketAvailable = true
|
||||||
|
for await (let e of window.client.eventStream()) {
|
||||||
|
handleEvent(e)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Websocket connection failed: ', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.websocketAvailable = false
|
||||||
|
console.log('Reconnecting websocket...')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEvent (msg) {
|
||||||
|
const typeName = msg.event.value.$typeName.replace('olivetin.api.v1.', '')
|
||||||
|
|
||||||
|
console.log("Websocket event receved: ", typeName)
|
||||||
|
|
||||||
|
const j = new Event(typeName)
|
||||||
|
j.payload = msg.event.value
|
||||||
|
|
||||||
|
switch (typeName) {
|
||||||
|
case 'EventOutputChunk':
|
||||||
|
case 'EventConfigChanged':
|
||||||
|
case 'EventEntityChanged':
|
||||||
|
case 'EventExecutionFinished':
|
||||||
|
case 'EventExecutionStarted':
|
||||||
|
window.dispatchEvent(j)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn('Unhandled websocket message type from server: ', typeName)
|
||||||
|
|
||||||
|
window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + typeName, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
import { createClient } from '@connectrpc/connect'
|
||||||
|
import { createConnectTransport } from '@connectrpc/connect-web'
|
||||||
|
|
||||||
|
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import router from './resources/vue/router.js';
|
||||||
|
import App from './resources/vue/App.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
initMarshaller,
|
initMarshaller,
|
||||||
setupSectionNavigation,
|
setupSectionNavigation,
|
||||||
marshalDashboardComponentsJsonToHtml,
|
marshalDashboardComponentsJsonToHtml,
|
||||||
marshalLogsJsonToHtml,
|
|
||||||
refreshServerConnectionLabel
|
refreshServerConnectionLabel
|
||||||
} from './js/marshaller.js'
|
} from './js/marshaller.js'
|
||||||
import { checkWebsocketConnection } from './js/websocket.js'
|
|
||||||
|
|
||||||
import { LoginForm } from './js/LoginForm.js'
|
import { checkWebsocketConnection } from './js/websocket.js'
|
||||||
|
|
||||||
function searchLogs (e) {
|
function searchLogs (e) {
|
||||||
document.getElementById('searchLogsClear').disabled = false
|
document.getElementById('searchLogsClear').disabled = false
|
||||||
|
|
@ -32,63 +39,24 @@ function searchLogsClear () {
|
||||||
document.getElementById('logSearchBox').value = ''
|
document.getElementById('logSearchBox').value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLogSearchBox () {
|
|
||||||
document.getElementById('logSearchBox').oninput = searchLogs
|
|
||||||
document.getElementById('searchLogsClear').onclick = searchLogsClear
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshLoop () {
|
function refreshLoop () {
|
||||||
if (window.websocketAvailable) {
|
|
||||||
// Websocket updates are streamed live, not updated on a loop.
|
|
||||||
} else if (window.restAvailable) {
|
|
||||||
// Fallback to rest, but try to reconnect the websocket anyway.
|
|
||||||
|
|
||||||
fetchGetDashboardComponents()
|
|
||||||
fetchGetLogs()
|
|
||||||
|
|
||||||
checkWebsocketConnection()
|
checkWebsocketConnection()
|
||||||
} else {
|
// fetchGetDashboardComponents()
|
||||||
// Still try to fetch the dashboard, if successfull window.restAvailable = true
|
// fetchGetLogs()
|
||||||
fetchGetDashboardComponents()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshServerConnectionLabel()
|
refreshServerConnectionLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchGetDashboardComponents () {
|
async function fetchGetDashboardComponents () {
|
||||||
window.fetch(window.restBaseUrl + 'GetDashboardComponents', {
|
try {
|
||||||
cors: 'cors'
|
const res = await window.client.getDashboardComponents()
|
||||||
}).then(res => {
|
|
||||||
if (!res.ok && res.status === 403) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
}).then(res => {
|
|
||||||
if (!window.restAvailable) {
|
|
||||||
window.clearBigErrors()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.restAvailable = true
|
|
||||||
marshalDashboardComponentsJsonToHtml(res)
|
marshalDashboardComponentsJsonToHtml(res)
|
||||||
|
|
||||||
refreshServerConnectionLabel() // in-case it changed, update the label quicker
|
refreshServerConnectionLabel() // in-case it changed, update the label quicker
|
||||||
}).catch((err) => { // err is 1st arg
|
} catch(err) {
|
||||||
window.restAvailable = false
|
|
||||||
window.showBigError('fetch-buttons', 'getting buttons', err, false)
|
window.showBigError('fetch-buttons', 'getting buttons', err, false)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchGetLogs () {
|
|
||||||
window.fetch(window.restBaseUrl + 'GetLogs', {
|
|
||||||
cors: 'cors'
|
|
||||||
}).then(res => {
|
|
||||||
return res.json()
|
|
||||||
}).then(res => {
|
|
||||||
marshalLogsJsonToHtml(res)
|
|
||||||
}).catch(err => {
|
|
||||||
window.showBigError('fetch-buttons', 'getting buttons', err, false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processWebuiSettingsJson (settings) {
|
function processWebuiSettingsJson (settings) {
|
||||||
|
|
@ -130,13 +98,6 @@ function processWebuiSettingsJson (settings) {
|
||||||
|
|
||||||
processAdditionalLinks(settings.AdditionalLinks)
|
processAdditionalLinks(settings.AdditionalLinks)
|
||||||
|
|
||||||
const loginForm = new LoginForm()
|
|
||||||
loginForm.setup()
|
|
||||||
loginForm.processOAuth2Providers(settings.AuthOAuth2Providers)
|
|
||||||
loginForm.processLocalLogin(settings.AuthLocalLogin)
|
|
||||||
|
|
||||||
document.getElementsByTagName('main')[0].appendChild(loginForm)
|
|
||||||
|
|
||||||
window.settings = settings
|
window.settings = settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,10 +126,28 @@ function processAdditionalLinks (links) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function main () {
|
function initClient () {
|
||||||
initMarshaller()
|
const transport = createConnectTransport({
|
||||||
|
baseUrl: window.location.protocol + '//' + window.location.host + '/api/',
|
||||||
|
|
||||||
setupLogSearchBox()
|
})
|
||||||
|
|
||||||
|
window.client = createClient(OliveTinApiService, transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVue () {
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
app.mount('#app')
|
||||||
|
}
|
||||||
|
|
||||||
|
function main () {
|
||||||
|
setupVue();
|
||||||
|
|
||||||
|
initClient()
|
||||||
|
|
||||||
|
initMarshaller()
|
||||||
|
|
||||||
window.addEventListener('EventConfigChanged', fetchGetDashboardComponents)
|
window.addEventListener('EventConfigChanged', fetchGetDashboardComponents)
|
||||||
window.addEventListener('EventEntityChanged', fetchGetDashboardComponents)
|
window.addEventListener('EventEntityChanged', fetchGetDashboardComponents)
|
||||||
|
|
@ -178,6 +157,8 @@ function main () {
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
processWebuiSettingsJson(res)
|
processWebuiSettingsJson(res)
|
||||||
|
|
||||||
|
fetchGetDashboardComponents()
|
||||||
|
|
||||||
window.restAvailable = true
|
window.restAvailable = true
|
||||||
window.refreshLoop = refreshLoop
|
window.refreshLoop = refreshLoop
|
||||||
window.refreshLoop()
|
window.refreshLoop()
|
||||||
|
|
@ -10,8 +10,6 @@
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.3.1",
|
"eslint-plugin-promise": "^4.3.1",
|
||||||
"parcel": "^2.11.0",
|
|
||||||
"parcel-resolver-ignore": "^2.2.0",
|
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^15.6.0",
|
"stylelint": "^15.6.0",
|
||||||
"stylelint-config-standard": "^33.0.0"
|
"stylelint-config-standard": "^33.0.0"
|
||||||
|
|
@ -29,7 +27,14 @@
|
||||||
],
|
],
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@connectrpc/connect": "^2.0.3",
|
||||||
|
"@connectrpc/connect-web": "^2.0.3",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"@xterm/addon-fit": "^0.10.0"
|
"femtocrank": "^1.2.2",
|
||||||
|
"unplugin-vue-components": "^28.8.0",
|
||||||
|
"vite": "^7.0.6",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
<template>
|
||||||
|
<div :id="`actionButton-${actionId}`" role="none" class="action-button">
|
||||||
|
<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
|
||||||
|
:class="buttonClasses" @click="handleClick">
|
||||||
|
<span class="icon" v-html="unicodeIcon"></span>
|
||||||
|
<span class="title" aria-live="polite">{{ displayTitle }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ArgumentForm v-if="showArgumentForm" :action-data="actionData" @submit="handleArgumentSubmit"
|
||||||
|
@cancel="handleArgumentCancel" @close="handleArgumentClose" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ArgumentForm from './ArgumentForm.vue'
|
||||||
|
|
||||||
|
import { ref, computed, watch, onMounted, inject } from 'vue'
|
||||||
|
|
||||||
|
const executionDialog = inject('executionDialog');
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const actionId = ref('')
|
||||||
|
const title = ref('')
|
||||||
|
const canExec = ref(true)
|
||||||
|
const popupOnStart = ref('')
|
||||||
|
|
||||||
|
// Display properties
|
||||||
|
const unicodeIcon = ref('💩')
|
||||||
|
const displayTitle = ref('')
|
||||||
|
|
||||||
|
// State
|
||||||
|
const isDisabled = ref(false)
|
||||||
|
const showArgumentForm = ref(false)
|
||||||
|
|
||||||
|
// Animation classes
|
||||||
|
const buttonClasses = ref([])
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
const updateIterationTimestamp = ref(0)
|
||||||
|
|
||||||
|
function getUnicodeIcon(icon) {
|
||||||
|
if (icon === '') {
|
||||||
|
return '💩'
|
||||||
|
} else {
|
||||||
|
return unescape(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructFromJson(json) {
|
||||||
|
updateIterationTimestamp.value = 0
|
||||||
|
|
||||||
|
// Class attributes
|
||||||
|
updateFromJson(json)
|
||||||
|
|
||||||
|
actionId.value = json.id
|
||||||
|
title.value = json.title
|
||||||
|
canExec.value = json.canExec
|
||||||
|
popupOnStart.value = json.popupOnStart
|
||||||
|
|
||||||
|
isDisabled.value = !json.canExec
|
||||||
|
displayTitle.value = title.value
|
||||||
|
unicodeIcon.value = getUnicodeIcon(json.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromJson(json) {
|
||||||
|
// Fields that should not be updated
|
||||||
|
// title - as the callback URL relies on it
|
||||||
|
|
||||||
|
unicodeIcon.value = getUnicodeIcon(json.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClick() {
|
||||||
|
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
|
||||||
|
updateUrlWithAction()
|
||||||
|
showArgumentForm.value = true
|
||||||
|
} else {
|
||||||
|
await startAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrlWithAction() {
|
||||||
|
// Get the current URL and create a new URL object
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
|
||||||
|
// Set the action parameter
|
||||||
|
url.searchParams.set('action', title.value)
|
||||||
|
|
||||||
|
// Update the URL without reloading the page
|
||||||
|
window.history.replaceState({}, '', url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUniqueId() {
|
||||||
|
if (window.isSecureContext) {
|
||||||
|
return window.crypto.randomUUID()
|
||||||
|
} else {
|
||||||
|
return Date.now().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startAction(actionArgs) {
|
||||||
|
buttonClasses.value = [] // Removes old animation classes
|
||||||
|
|
||||||
|
if (actionArgs === undefined) {
|
||||||
|
actionArgs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUIDs are create client side, so that we can setup a "execution-button"
|
||||||
|
// to track the execution before we send the request to the server.
|
||||||
|
const startActionArgs = {
|
||||||
|
actionId: actionId.value,
|
||||||
|
arguments: actionArgs,
|
||||||
|
uniqueTrackingId: getUniqueId()
|
||||||
|
}
|
||||||
|
|
||||||
|
onActionStarted(startActionArgs.uniqueTrackingId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.client.startAction(startActionArgs)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to start action:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActionStarted(execTrackingId) {
|
||||||
|
console.log('onActionStarted', execTrackingId)
|
||||||
|
console.log('executionDialog', executionDialog)
|
||||||
|
|
||||||
|
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||||
|
if (executionDialog.value) {
|
||||||
|
executionDialog.value.reset();
|
||||||
|
|
||||||
|
if (popupOnStart.value === 'execution-dialog-stdout-only') {
|
||||||
|
executionDialog.value.hideEverythingApartFromOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executionDialog.value.executionTrackingId = execTrackingId;
|
||||||
|
executionDialog.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisabled.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArgumentSubmit(args) {
|
||||||
|
startAction(args)
|
||||||
|
showArgumentForm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArgumentCancel() {
|
||||||
|
showArgumentForm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArgumentClose() {
|
||||||
|
showArgumentForm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionFeedbackButton methods
|
||||||
|
function onExecutionFinished(logEntry) {
|
||||||
|
if (logEntry.timedOut) {
|
||||||
|
renderExecutionResult('action-timeout', 'Timed out')
|
||||||
|
} else if (logEntry.blocked) {
|
||||||
|
renderExecutionResult('action-blocked', 'Blocked!')
|
||||||
|
} else if (logEntry.exitCode !== 0) {
|
||||||
|
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||||
|
} else {
|
||||||
|
const ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
|
||||||
|
renderExecutionResult('action-success', 'Success!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
||||||
|
updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||||
|
onExecStatusChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDom(resultCssClass, newTitle) {
|
||||||
|
if (resultCssClass == null) {
|
||||||
|
buttonClasses.value = []
|
||||||
|
} else {
|
||||||
|
buttonClasses.value = [resultCssClass]
|
||||||
|
}
|
||||||
|
|
||||||
|
displayTitle.value = newTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExecStatusChanged() {
|
||||||
|
isDisabled.value = false
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
updateDom(null, title.value)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
constructFromJson(props.actionData)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.actionData,
|
||||||
|
(newData) => {
|
||||||
|
updateFromJson(newData)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onExecutionFinished
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 0 .6em #aaa;
|
||||||
|
font-size: .85em;
|
||||||
|
border-radius: .7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button:hover:not(:disabled) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button .icon {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button .title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation classes */
|
||||||
|
.action-button button.action-timeout {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-color: #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button.action-blocked {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button.action-nonzero-exit {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button button.action-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button-footer {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<img src="../../OliveTinLogo.png" alt="OliveTin logo" class="logo" />
|
||||||
|
|
||||||
|
<h1 id="page-title">
|
||||||
|
<router-link to="/">OliveTin</router-link>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<button id="sidebar-toggler-button" aria-label="Open sidebar navigation" aria-pressed="false"
|
||||||
|
aria-haspopup="menu" @click="toggleSidebar">☰</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="fg1" />
|
||||||
|
|
||||||
|
<div class="userinfo">
|
||||||
|
<span id="link-login" hidden><router-link to="/login">Login</router-link> |</span>
|
||||||
|
<span id="link-logout" hidden><a href="/api/Logout">Logout</a> |</span>
|
||||||
|
<span id="username"> </span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path
|
||||||
|
d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="layout">
|
||||||
|
<Sidebar ref="sidebar" />
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<main title="Main content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ExecutionDialog ref="executionDialog" />
|
||||||
|
|
||||||
|
<footer title="footer">
|
||||||
|
<p><img title="application icon" src="../../OliveTinLogo.png" alt="OliveTin logo" height="1em"
|
||||||
|
class="logo" />
|
||||||
|
OliveTin</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<a href="https://docs.olivetin.app" target="_new">Documentation</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">Raise an issue on
|
||||||
|
GitHub</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span id="currentVersion">?</span>
|
||||||
|
|
||||||
|
<span id="serverConnectionRest">REST</span>
|
||||||
|
<span id="serverConnectionWebSocket">WebSocket</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Sidebar from './components/Sidebar.vue';
|
||||||
|
import ExecutionDialog from './ExecutionDialog.vue';
|
||||||
|
|
||||||
|
import { provide } from 'vue';
|
||||||
|
const sidebar = ref(null);
|
||||||
|
const executionDialog = ref(null);
|
||||||
|
|
||||||
|
provide('executionDialog', executionDialog.value);
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
if (sidebar.value && typeof sidebar.value.isOpen !== 'undefined') {
|
||||||
|
sidebar.value.isOpen = !sidebar.value.isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,432 @@
|
||||||
|
<template>
|
||||||
|
<dialog
|
||||||
|
ref="dialog"
|
||||||
|
title="Arguments"
|
||||||
|
class="action-arguments"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<form class="padded-content" @submit.prevent="handleSubmit">
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="action-header">
|
||||||
|
<span class="icon" v-html="icon"></span>
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="arguments">
|
||||||
|
<div
|
||||||
|
v-for="arg in arguments"
|
||||||
|
:key="arg.name"
|
||||||
|
class="argument-group"
|
||||||
|
>
|
||||||
|
<label :for="arg.name">
|
||||||
|
{{ formatLabel(arg.title) }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<datalist
|
||||||
|
v-if="arg.suggestions && Object.keys(arg.suggestions).length > 0"
|
||||||
|
:id="`${arg.name}-choices`"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="(suggestion, key) in arg.suggestions"
|
||||||
|
:key="key"
|
||||||
|
:value="key"
|
||||||
|
>
|
||||||
|
{{ suggestion }}
|
||||||
|
</option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="getInputComponent(arg)"
|
||||||
|
:id="arg.name"
|
||||||
|
:name="arg.name"
|
||||||
|
:value="getArgumentValue(arg)"
|
||||||
|
:list="arg.suggestions ? `${arg.name}-choices` : undefined"
|
||||||
|
:type="getInputType(arg)"
|
||||||
|
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
||||||
|
:step="arg.type === 'datetime' ? 1 : undefined"
|
||||||
|
:pattern="getPattern(arg)"
|
||||||
|
:required="arg.required"
|
||||||
|
@input="handleInput(arg, $event)"
|
||||||
|
@change="handleChange(arg, $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="arg.description"
|
||||||
|
class="argument-description"
|
||||||
|
v-html="arg.description"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
name="start"
|
||||||
|
type="submit"
|
||||||
|
:disabled="!isFormValid || (hasConfirmation && !confirmationChecked)"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
name="cancel"
|
||||||
|
type="button"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ArgumentForm',
|
||||||
|
props: {
|
||||||
|
actionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
icon: '',
|
||||||
|
arguments: [],
|
||||||
|
argValues: {},
|
||||||
|
confirmationChecked: false,
|
||||||
|
hasConfirmation: false,
|
||||||
|
formErrors: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isFormValid() {
|
||||||
|
return Object.keys(this.formErrors).length === 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setup()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setup() {
|
||||||
|
this.title = this.actionData.title
|
||||||
|
this.icon = this.actionData.icon
|
||||||
|
this.arguments = this.actionData.arguments || []
|
||||||
|
this.argValues = {}
|
||||||
|
this.formErrors = {}
|
||||||
|
this.confirmationChecked = false
|
||||||
|
this.hasConfirmation = false
|
||||||
|
|
||||||
|
// Initialize values from query params or defaults
|
||||||
|
this.arguments.forEach(arg => {
|
||||||
|
const paramValue = this.getQueryParamValue(arg.name)
|
||||||
|
this.argValues[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
|
||||||
|
|
||||||
|
if (arg.type === 'confirmation') {
|
||||||
|
this.hasConfirmation = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getQueryParamValue(paramName) {
|
||||||
|
const params = new URLSearchParams(window.location.search.substring(1))
|
||||||
|
return params.get(paramName)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatLabel(title) {
|
||||||
|
const lastChar = title.charAt(title.length - 1)
|
||||||
|
if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return title + ':'
|
||||||
|
},
|
||||||
|
|
||||||
|
getInputComponent(arg) {
|
||||||
|
if (arg.type === 'html') {
|
||||||
|
return 'div'
|
||||||
|
} else if (arg.type === 'raw_string_multiline') {
|
||||||
|
return 'textarea'
|
||||||
|
} else if (arg.choices && arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) {
|
||||||
|
return 'select'
|
||||||
|
} else {
|
||||||
|
return 'input'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInputType(arg) {
|
||||||
|
if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return arg.type
|
||||||
|
},
|
||||||
|
|
||||||
|
getPattern(arg) {
|
||||||
|
if (arg.type && arg.type.startsWith('regex:')) {
|
||||||
|
return arg.type.replace('regex:', '')
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
getArgumentValue(arg) {
|
||||||
|
if (arg.type === 'checkbox') {
|
||||||
|
return this.argValues[arg.name] === '1' || this.argValues[arg.name] === true
|
||||||
|
}
|
||||||
|
return this.argValues[arg.name] || ''
|
||||||
|
},
|
||||||
|
|
||||||
|
handleInput(arg, event) {
|
||||||
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||||
|
this.argValues[arg.name] = value
|
||||||
|
this.updateUrlWithArg(arg.name, value)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange(arg, event) {
|
||||||
|
if (arg.type === 'confirmation') {
|
||||||
|
this.confirmationChecked = event.target.checked
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the input
|
||||||
|
this.validateArgument(arg, event.target.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
async validateArgument(arg, value) {
|
||||||
|
if (!arg.type || arg.type.startsWith('regex:')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validateArgumentTypeArgs = {
|
||||||
|
value: value,
|
||||||
|
type: arg.type
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await window.validateArgumentType(validateArgumentTypeArgs)
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
this.$delete(this.formErrors, arg.name)
|
||||||
|
} else {
|
||||||
|
this.$set(this.formErrors, arg.name, validation.description)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Validation failed:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUrlWithArg(name, value) {
|
||||||
|
if (name && value !== undefined) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
|
||||||
|
// Don't add passwords to URL
|
||||||
|
const arg = this.arguments.find(a => a.name === name)
|
||||||
|
if (arg && arg.type === 'password') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url.searchParams.set(name, value)
|
||||||
|
window.history.replaceState({}, '', url.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getArgumentValues() {
|
||||||
|
const ret = []
|
||||||
|
|
||||||
|
for (const arg of this.arguments) {
|
||||||
|
let value = this.argValues[arg.name] || ''
|
||||||
|
|
||||||
|
if (arg.type === 'checkbox') {
|
||||||
|
value = value ? '1' : '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push({
|
||||||
|
name: arg.name,
|
||||||
|
value: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
// Validate all inputs
|
||||||
|
let isValid = true
|
||||||
|
|
||||||
|
for (const arg of this.arguments) {
|
||||||
|
const value = this.argValues[arg.name]
|
||||||
|
if (arg.required && (!value || value === '')) {
|
||||||
|
this.$set(this.formErrors, arg.name, 'This field is required')
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const argvs = this.getArgumentValues()
|
||||||
|
this.$emit('submit', argvs)
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCancel() {
|
||||||
|
this.clearBookmark()
|
||||||
|
this.$emit('cancel')
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
|
||||||
|
clearBookmark() {
|
||||||
|
// Remove the action from the URL
|
||||||
|
window.history.replaceState({
|
||||||
|
path: window.location.pathname
|
||||||
|
}, '', window.location.pathname)
|
||||||
|
},
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.$refs.dialog.showModal()
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$refs.dialog.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action-arguments {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-header .icon {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arguments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group input,
|
||||||
|
.argument-group select,
|
||||||
|
.argument-group textarea {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group input:focus,
|
||||||
|
.argument-group select:focus,
|
||||||
|
.argument-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group input:invalid,
|
||||||
|
.argument-group select:invalid,
|
||||||
|
.argument-group textarea:invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button:hover:not(:disabled) {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button[name="start"] {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button[name="start"]:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
border-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox specific styling */
|
||||||
|
.argument-group input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument-group input[type="checkbox"] + label {
|
||||||
|
display: inline;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<section class = "transparent">
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ dashboard.title }}</legend>
|
||||||
|
|
||||||
|
<ActionButton :actionData = "action" v-for = "action in dashboard.contents" :key = "action.id" />
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ActionButton from './ActionButton.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
dashboard: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
fieldset {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, 180px);
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
justify-content: center;
|
||||||
|
place-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:id="`execution-${executionTrackingId}`"
|
||||||
|
class="execution-button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
:title="`${ellapsed}s`"
|
||||||
|
@click="show"
|
||||||
|
>
|
||||||
|
{{ buttonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ExecutionButton',
|
||||||
|
// mixins: [ExecutionFeedbackButton],
|
||||||
|
props: {
|
||||||
|
executionTrackingId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ellapsed: 0,
|
||||||
|
isWaiting: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
buttonText() {
|
||||||
|
if (this.isWaiting) {
|
||||||
|
return 'Executing...'
|
||||||
|
} else {
|
||||||
|
return `${this.ellapsed}s`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.constructFromJson(this.executionTrackingId)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
constructFromJson(json) {
|
||||||
|
this.executionTrackingId = json
|
||||||
|
this.ellapsed = 0
|
||||||
|
this.isWaiting = true
|
||||||
|
},
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.$emit('show')
|
||||||
|
|
||||||
|
if (window.executionDialog) {
|
||||||
|
window.executionDialog.reset()
|
||||||
|
window.executionDialog.show()
|
||||||
|
window.executionDialog.fetchExecutionResult(this.executionTrackingId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onExecStatusChanged() {
|
||||||
|
this.isWaiting = false
|
||||||
|
this.domTitle = this.ellapsed + 's'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Override from ExecutionFeedbackButton
|
||||||
|
onExecutionFinished(logEntry) {
|
||||||
|
if (logEntry.timedOut) {
|
||||||
|
this.renderExecutionResult('action-timeout', 'Timed out')
|
||||||
|
} else if (logEntry.blocked) {
|
||||||
|
this.renderExecutionResult('action-blocked', 'Blocked!')
|
||||||
|
} else if (logEntry.exitCode !== 0) {
|
||||||
|
this.renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||||
|
} else {
|
||||||
|
this.ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
|
||||||
|
this.renderExecutionResult('action-success', 'Success!')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
||||||
|
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||||
|
this.onExecStatusChanged()
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDom(resultCssClass, title) {
|
||||||
|
// For execution button, we don't need to update classes as much
|
||||||
|
// since it's a simpler component
|
||||||
|
if (resultCssClass) {
|
||||||
|
this.$el.classList.add(resultCssClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.execution-button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-button button {
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-button button:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation classes */
|
||||||
|
.execution-button button.action-timeout {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-color: #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-button button.action-blocked {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-button button.action-nonzero-exit {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-button button.action-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,421 @@
|
||||||
|
<template>
|
||||||
|
<dialog
|
||||||
|
ref="dialog"
|
||||||
|
title="Execution Results"
|
||||||
|
:class="{ big: isBig }"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="action-header padded-content">
|
||||||
|
<span class="icon" role="img" v-html="icon"></span>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
<span :title="titleTooltip">{{ title }}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-show="!hideToggleButton"
|
||||||
|
@click="toggleSize"
|
||||||
|
title="Toggle dialog size"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!hideBasics" class="padded-content-sides">
|
||||||
|
<strong>Duration: </strong><span v-html="duration"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!hideDetails && logEntry" class="padded-content-sides">
|
||||||
|
<p>
|
||||||
|
<strong>Status: </strong>
|
||||||
|
<ActionStatusDisplay :log-entry="logEntry" v-if="logEntry"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="xtermOutput" v-show="!isHtmlOutput"></div>
|
||||||
|
<div
|
||||||
|
v-show="isHtmlOutput"
|
||||||
|
class="padded-content"
|
||||||
|
v-html="htmlOutput"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="buttons padded-content">
|
||||||
|
<button
|
||||||
|
:disabled="!canRerun"
|
||||||
|
@click="rerunAction"
|
||||||
|
title="Rerun"
|
||||||
|
>
|
||||||
|
Rerun
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:disabled="!canKill"
|
||||||
|
@click="killAction"
|
||||||
|
title="Kill"
|
||||||
|
>
|
||||||
|
Kill
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form method="dialog">
|
||||||
|
<button name="Cancel" title="Close">Close</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
import ActionStatusDisplay from './components/ActionStatusDisplay.vue'
|
||||||
|
import { OutputTerminal } from '../../js/OutputTerminal.js'
|
||||||
|
|
||||||
|
// Refs for DOM elements
|
||||||
|
const xtermOutput = ref(null)
|
||||||
|
const dialog = ref(null)
|
||||||
|
|
||||||
|
// State
|
||||||
|
const state = reactive({
|
||||||
|
isBig: false,
|
||||||
|
hideToggleButton: false,
|
||||||
|
hideBasics: false,
|
||||||
|
hideDetails: false,
|
||||||
|
hideDetailsOnResult: false,
|
||||||
|
|
||||||
|
// Execution data
|
||||||
|
executionSeconds: 0,
|
||||||
|
executionTrackingId: 'notset',
|
||||||
|
executionTicker: null,
|
||||||
|
|
||||||
|
// Display data
|
||||||
|
icon: '',
|
||||||
|
title: 'Waiting for result...',
|
||||||
|
titleTooltip: '',
|
||||||
|
duration: '',
|
||||||
|
htmlOutput: '',
|
||||||
|
isHtmlOutput: false,
|
||||||
|
|
||||||
|
// Action data
|
||||||
|
logEntry: null,
|
||||||
|
canRerun: false,
|
||||||
|
canKill: false,
|
||||||
|
|
||||||
|
// Terminal
|
||||||
|
terminal: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expose for template
|
||||||
|
const isBig = ref(false)
|
||||||
|
const hideToggleButton = ref(false)
|
||||||
|
const hideBasics = ref(false)
|
||||||
|
const hideDetails = ref(false)
|
||||||
|
const hideDetailsOnResult = ref(false)
|
||||||
|
const executionSeconds = ref(0)
|
||||||
|
const executionTrackingId = ref('notset')
|
||||||
|
const icon = ref('')
|
||||||
|
const title = ref('Waiting for result...')
|
||||||
|
const titleTooltip = ref('')
|
||||||
|
const duration = ref('')
|
||||||
|
const htmlOutput = ref('')
|
||||||
|
const isHtmlOutput = ref(false)
|
||||||
|
const logEntry = ref(null)
|
||||||
|
const canRerun = ref(false)
|
||||||
|
const canKill = ref(false)
|
||||||
|
|
||||||
|
let executionTicker = null
|
||||||
|
let terminal = null
|
||||||
|
|
||||||
|
function syncStateToRefs() {
|
||||||
|
isBig.value = state.isBig
|
||||||
|
hideToggleButton.value = state.hideToggleButton
|
||||||
|
hideBasics.value = state.hideBasics
|
||||||
|
hideDetails.value = state.hideDetails
|
||||||
|
hideDetailsOnResult.value = state.hideDetailsOnResult
|
||||||
|
executionSeconds.value = state.executionSeconds
|
||||||
|
executionTrackingId.value = state.executionTrackingId
|
||||||
|
icon.value = state.icon
|
||||||
|
title.value = state.title
|
||||||
|
titleTooltip.value = state.titleTooltip
|
||||||
|
duration.value = state.duration
|
||||||
|
htmlOutput.value = state.htmlOutput
|
||||||
|
isHtmlOutput.value = state.isHtmlOutput
|
||||||
|
logEntry.value = state.logEntry
|
||||||
|
canRerun.value = state.canRerun
|
||||||
|
canKill.value = state.canKill
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRefsToState() {
|
||||||
|
state.isBig = isBig.value
|
||||||
|
state.hideToggleButton = hideToggleButton.value
|
||||||
|
state.hideBasics = hideBasics.value
|
||||||
|
state.hideDetails = hideDetails.value
|
||||||
|
state.hideDetailsOnResult = hideDetailsOnResult.value
|
||||||
|
state.executionSeconds = executionSeconds.value
|
||||||
|
state.executionTrackingId = executionTrackingId.value
|
||||||
|
state.icon = icon.value
|
||||||
|
state.title = title.value
|
||||||
|
state.titleTooltip = titleTooltip.value
|
||||||
|
state.duration = duration.value
|
||||||
|
state.htmlOutput = htmlOutput.value
|
||||||
|
state.isHtmlOutput = isHtmlOutput.value
|
||||||
|
state.logEntry = logEntry.value
|
||||||
|
state.canRerun = canRerun.value
|
||||||
|
state.canKill = canKill.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeTerminal() {
|
||||||
|
terminal = new OutputTerminal()
|
||||||
|
terminal.open(xtermOutput.value)
|
||||||
|
terminal.resize(80, 24)
|
||||||
|
window.terminal = terminal
|
||||||
|
state.terminal = terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSize() {
|
||||||
|
state.isBig = !state.isBig
|
||||||
|
isBig.value = state.isBig
|
||||||
|
if (state.isBig) {
|
||||||
|
terminal.fit()
|
||||||
|
} else {
|
||||||
|
terminal.resize(80, 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
state.executionSeconds = 0
|
||||||
|
state.executionTrackingId = 'notset'
|
||||||
|
state.isBig = false
|
||||||
|
state.hideToggleButton = false
|
||||||
|
state.hideBasics = false
|
||||||
|
state.hideDetails = false
|
||||||
|
state.hideDetailsOnResult = false
|
||||||
|
|
||||||
|
state.icon = ''
|
||||||
|
state.title = 'Waiting for result...'
|
||||||
|
state.titleTooltip = ''
|
||||||
|
state.duration = ''
|
||||||
|
state.htmlOutput = ''
|
||||||
|
state.isHtmlOutput = false
|
||||||
|
|
||||||
|
state.canRerun = false
|
||||||
|
state.canKill = false
|
||||||
|
state.logEntry = null
|
||||||
|
|
||||||
|
syncStateToRefs()
|
||||||
|
|
||||||
|
if (terminal) {
|
||||||
|
await terminal.reset()
|
||||||
|
terminal.fit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(actionButton) {
|
||||||
|
if (actionButton) {
|
||||||
|
state.icon = actionButton.domIcon.innerText
|
||||||
|
icon.value = state.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
state.canKill = true
|
||||||
|
canKill.value = true
|
||||||
|
|
||||||
|
// Clear existing ticker
|
||||||
|
if (executionTicker) {
|
||||||
|
clearInterval(executionTicker)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.executionSeconds = 0
|
||||||
|
executionSeconds.value = 0
|
||||||
|
executionTick()
|
||||||
|
executionTicker = setInterval(() => {
|
||||||
|
executionTick()
|
||||||
|
}, 1000)
|
||||||
|
state.executionTicker = executionTicker
|
||||||
|
|
||||||
|
// Close if already open
|
||||||
|
if (dialog.value && dialog.value.open) {
|
||||||
|
dialog.value.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.value && dialog.value.showModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerunAction() {
|
||||||
|
if (state.logEntry && state.logEntry.actionId) {
|
||||||
|
const actionButton = document.getElementById('actionButton-' + state.logEntry.actionId)
|
||||||
|
if (actionButton && actionButton.btn) {
|
||||||
|
actionButton.btn.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.value && dialog.value.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function killAction() {
|
||||||
|
if (!state.executionTrackingId || state.executionTrackingId === 'notset') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const killActionArgs = {
|
||||||
|
executionTrackingId: state.executionTrackingId
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.client.killAction(killActionArgs)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to kill action:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executionTick() {
|
||||||
|
state.executionSeconds++
|
||||||
|
executionSeconds.value = state.executionSeconds
|
||||||
|
updateDuration(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideEverythingApartFromOutput() {
|
||||||
|
state.hideDetailsOnResult = true
|
||||||
|
state.hideBasics = true
|
||||||
|
hideDetailsOnResult.value = true
|
||||||
|
hideBasics.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchExecutionResult(executionTrackingIdParam) {
|
||||||
|
state.executionTrackingId = executionTrackingIdParam
|
||||||
|
executionTrackingId.value = executionTrackingIdParam
|
||||||
|
|
||||||
|
const executionStatusArgs = {
|
||||||
|
executionTrackingId: state.executionTrackingId
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logEntryResult = await window.client.executionStatus(executionStatusArgs)
|
||||||
|
await renderExecutionResult(logEntryResult)
|
||||||
|
} catch (err) {
|
||||||
|
renderError(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDuration(logEntryParam) {
|
||||||
|
let logEntry = logEntryParam
|
||||||
|
if (logEntry == null) {
|
||||||
|
duration.value = state.executionSeconds + ' seconds'
|
||||||
|
state.duration = duration.value
|
||||||
|
} else if (!logEntry.executionStarted) {
|
||||||
|
duration.value = logEntry.datetimeStarted + ' (request time). Not executed.'
|
||||||
|
state.duration = duration.value
|
||||||
|
} else if (logEntry.executionStarted && !logEntry.executionFinished) {
|
||||||
|
duration.value = logEntry.datetimeStarted
|
||||||
|
state.duration = duration.value
|
||||||
|
} else {
|
||||||
|
let delta = ''
|
||||||
|
try {
|
||||||
|
delta = (new Date(logEntry.datetimeStarted) - new Date(logEntry.datetimeStarted)) / 1000
|
||||||
|
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to calculate delta', e)
|
||||||
|
}
|
||||||
|
duration.value = logEntry.datetimeStarted + ' → ' + logEntry.datetimeFinished
|
||||||
|
if (delta !== '') {
|
||||||
|
duration.value += ' (' + delta + ')'
|
||||||
|
}
|
||||||
|
state.duration = duration.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderExecutionResult(res) {
|
||||||
|
// Clear ticker
|
||||||
|
if (executionTicker) {
|
||||||
|
clearInterval(executionTicker)
|
||||||
|
}
|
||||||
|
state.executionTicker = null
|
||||||
|
|
||||||
|
if (res.type === 'execution-dialog-output-html') {
|
||||||
|
state.isHtmlOutput = true
|
||||||
|
state.htmlOutput = res.logEntry.output
|
||||||
|
state.hideDetailsOnResult = true
|
||||||
|
isHtmlOutput.value = true
|
||||||
|
htmlOutput.value = res.logEntry.output
|
||||||
|
hideDetailsOnResult.value = true
|
||||||
|
} else {
|
||||||
|
state.isHtmlOutput = false
|
||||||
|
state.htmlOutput = ''
|
||||||
|
isHtmlOutput.value = false
|
||||||
|
htmlOutput.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.hideDetailsOnResult) {
|
||||||
|
state.hideDetails = true
|
||||||
|
hideDetails.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
state.executionTrackingId = res.logEntry.executionTrackingId
|
||||||
|
executionTrackingId.value = res.logEntry.executionTrackingId
|
||||||
|
state.canRerun = res.logEntry.executionFinished
|
||||||
|
canRerun.value = res.logEntry.executionFinished
|
||||||
|
state.canKill = res.logEntry.canKill
|
||||||
|
canKill.value = res.logEntry.canKill
|
||||||
|
state.logEntry = res.logEntry
|
||||||
|
logEntry.value = res.logEntry
|
||||||
|
|
||||||
|
state.icon = res.logEntry.actionIcon
|
||||||
|
icon.value = res.logEntry.actionIcon
|
||||||
|
state.title = res.logEntry.actionTitle
|
||||||
|
title.value = res.logEntry.actionTitle
|
||||||
|
state.titleTooltip = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId
|
||||||
|
titleTooltip.value = state.titleTooltip
|
||||||
|
|
||||||
|
updateDuration(res.logEntry)
|
||||||
|
|
||||||
|
if (terminal) {
|
||||||
|
await terminal.reset()
|
||||||
|
await terminal.write(res.logEntry.output, () => {
|
||||||
|
terminal.fit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderError(err) {
|
||||||
|
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
// Clean up when dialog is closed
|
||||||
|
if (executionTicker) {
|
||||||
|
clearInterval(executionTicker)
|
||||||
|
}
|
||||||
|
state.executionTicker = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (executionTicker) {
|
||||||
|
clearInterval(executionTicker)
|
||||||
|
}
|
||||||
|
state.executionTicker = null
|
||||||
|
if (terminal) {
|
||||||
|
terminal.close()
|
||||||
|
}
|
||||||
|
state.terminal = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
initializeTerminal()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expose methods for parent/imperative use
|
||||||
|
defineExpose({
|
||||||
|
reset,
|
||||||
|
show,
|
||||||
|
rerunAction,
|
||||||
|
killAction,
|
||||||
|
fetchExecutionResult,
|
||||||
|
renderExecutionResult,
|
||||||
|
hideEverythingApartFromOutput,
|
||||||
|
handleClose
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export class LoginForm extends window.HTMLElement {
|
<script setup>
|
||||||
setup () {
|
setup () {
|
||||||
const tpl = document.getElementById('tplLoginForm')
|
const tpl = document.getElementById('tplLoginForm')
|
||||||
this.content = tpl.content.cloneNode(true)
|
this.content = tpl.content.cloneNode(true)
|
||||||
|
|
@ -11,40 +11,24 @@ export class LoginForm extends window.HTMLElement {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
localLoginRequest () {
|
async localLoginRequest () {
|
||||||
const username = this.querySelector('input.username').value
|
const username = this.querySelector('input.username').value
|
||||||
const password = this.querySelector('input.password').value
|
const password = this.querySelector('input.password').value
|
||||||
|
|
||||||
document.querySelector('.error').innerHTML = ''
|
document.querySelector('.error').innerHTML = ''
|
||||||
|
|
||||||
window.fetch('/api/LocalUserLogin', {
|
const args = {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password
|
||||||
})
|
}
|
||||||
}).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
const res = response.json()
|
|
||||||
|
|
||||||
if (res !== undefined) {
|
const loginResult = await window.client.localUserLogin(args)
|
||||||
return res
|
|
||||||
} else {
|
if (loginResult.success) {
|
||||||
throw new Error('Failed to login - no res')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to login')
|
|
||||||
}
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
window.location.href = '/'
|
window.location.href = '/'
|
||||||
} else {
|
} else {
|
||||||
document.querySelector('.error').innerHTML = 'Login failed.'
|
document.querySelector('.error').innerHTML = 'Login failed.'
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processOAuth2Providers (providers) {
|
processOAuth2Providers (providers) {
|
||||||
|
|
@ -85,6 +69,4 @@ export class LoginForm extends window.HTMLElement {
|
||||||
this.querySelector('.login-disabled').hidden = true
|
this.querySelector('.login-disabled').hidden = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
</script>
|
||||||
|
|
||||||
window.customElements.define('login-form', LoginForm)
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<section title="Logs" class="">
|
||||||
|
<div class="toolbar">
|
||||||
|
<label class="input-with-icons">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||||
|
</svg>
|
||||||
|
<input placeholder="Search for action name" id="logSearchBox" />
|
||||||
|
<button id="searchLogsClear" title="Clear search filter" disabled>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<table id="logsTable" title="Logs" hidden>
|
||||||
|
<thead>
|
||||||
|
<tr title="untitled">
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Metadata</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="logEntry in logEntries" :key="logEntry.executionTrackingId"></tr>
|
||||||
|
<td>{{ logEntry.datetimeStarted }}</td>
|
||||||
|
<td>{{ logEntry.actionTitle }}</td>
|
||||||
|
<td>{{ logEntry.actionIcon }}</td>
|
||||||
|
<td>{{ logEntry.tags }}</td>
|
||||||
|
<td>{{ logEntry.user }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p id="logsTableEmpty">There are no logs to display. <a href="/">Return to index</a></p>
|
||||||
|
|
||||||
|
<p><strong>Note:</strong> The server is configured to only send <strong id="logs-server-page-size">?</strong>
|
||||||
|
log entries at a time. The search box at the top of this page only searches this current page of logs.</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
function setupLogSearchBox () {
|
||||||
|
document.getElementById('logSearchBox').oninput = searchLogs
|
||||||
|
document.getElementById('searchLogsClear').onclick = searchLogsClear
|
||||||
|
}
|
||||||
|
|
||||||
|
function marshalLogsJsonToHtml (json) {
|
||||||
|
// This function is called internally with a "fake" server response, that does
|
||||||
|
// not have pageSize set. So we need to check if it's set before trying to use it.
|
||||||
|
if (json.pageSize !== undefined) {
|
||||||
|
document.getElementById('logs-server-page-size').innerText = json.pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.logs != null && json.logs.length > 0) {
|
||||||
|
document.getElementById('logsTable').hidden = false
|
||||||
|
document.getElementById('logsTableEmpty').hidden = true
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const logEntry of json.logs) {
|
||||||
|
let row = document.getElementById('log-' + logEntry.executionTrackingId)
|
||||||
|
|
||||||
|
if (row == null) {
|
||||||
|
const tpl = document.getElementById('tplLogRow')
|
||||||
|
row = tpl.content.querySelector('tr').cloneNode(true)
|
||||||
|
row.id = 'log-' + logEntry.executionTrackingId
|
||||||
|
|
||||||
|
row.querySelector('.content').onclick = () => {
|
||||||
|
window.executionDialog.reset()
|
||||||
|
window.executionDialog.show()
|
||||||
|
window.executionDialog.renderExecutionResult({
|
||||||
|
logEntry: window.logEntries.get(logEntry.executionTrackingId)
|
||||||
|
})
|
||||||
|
pushNewNavigationPath('/logs/' + logEntry.executionTrackingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry.dom = row
|
||||||
|
|
||||||
|
window.logEntries.set(logEntry.executionTrackingId, logEntry)
|
||||||
|
|
||||||
|
document.querySelector('#logTableBody').prepend(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
|
||||||
|
row.querySelector('.content').innerText = logEntry.actionTitle
|
||||||
|
row.querySelector('.icon').innerHTML = logEntry.actionIcon
|
||||||
|
row.setAttribute('title', logEntry.actionTitle)
|
||||||
|
|
||||||
|
row.exitCodeDisplay.update(logEntry)
|
||||||
|
|
||||||
|
row.querySelector('.tags').innerHTML = ''
|
||||||
|
|
||||||
|
for (const tag of logEntry.tags) {
|
||||||
|
row.querySelector('.tags').append(createTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
row.querySelector('.tags').append(createAnnotation('user', logEntry.user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupLogSearchBox()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<span :class="['action-status', statusClass]">{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
logEntry: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const logEntry = props.logEntry
|
||||||
|
if (!logEntry) return 'unknown'
|
||||||
|
|
||||||
|
if (logEntry.executionFinished) {
|
||||||
|
if (logEntry.blocked) {
|
||||||
|
return 'Blocked'
|
||||||
|
} else if (logEntry.timedOut) {
|
||||||
|
return 'Timed out'
|
||||||
|
} else {
|
||||||
|
return 'Completed'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'Still running...'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitCodeText = computed(() => {
|
||||||
|
const logEntry = props.logEntry
|
||||||
|
if (!logEntry) return ''
|
||||||
|
if (logEntry.executionFinished) {
|
||||||
|
if (logEntry.blocked || logEntry.timedOut) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return ' Exit code: ' + logEntry.exitCode
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const logEntry = props.logEntry
|
||||||
|
if (!logEntry) return ''
|
||||||
|
if (logEntry.executionFinished) {
|
||||||
|
if (logEntry.blocked) {
|
||||||
|
return 'action-blocked'
|
||||||
|
} else if (logEntry.timedOut) {
|
||||||
|
return 'action-timeout'
|
||||||
|
} else if (logEntry.exitCode === 0) {
|
||||||
|
return 'action-success'
|
||||||
|
} else {
|
||||||
|
return 'action-nonzero-exit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
<template>
|
||||||
|
<aside :class="{ 'shown': isOpen, 'stuck': isStuck }" class="sidebar">
|
||||||
|
<button
|
||||||
|
class="stick-toggle"
|
||||||
|
:aria-pressed="isStuck"
|
||||||
|
:title="isStuck ? 'Unstick sidebar' : 'Stick sidebar'"
|
||||||
|
@click="toggleStick"
|
||||||
|
>
|
||||||
|
<span v-if="isStuck">📌 Unstick</span>
|
||||||
|
<span v-else>📍 Stick</span>
|
||||||
|
</button>
|
||||||
|
<nav class="mainnav">
|
||||||
|
<ul class="navigation-links">
|
||||||
|
<li v-for="link in navigationLinks" :key="link.id" :title="link.title">
|
||||||
|
<router-link :to="link.path" :class="{ active: isActive(link.path) }">
|
||||||
|
<span v-if="link.icon" class="icon" v-html="link.icon"></span>
|
||||||
|
<span class="title">{{ link.title }}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="supplemental-links">
|
||||||
|
<li v-for="link in supplementalLinks" :key="link.id" :title="link.title">
|
||||||
|
<a :href="link.url" :target="link.target || '_self'">
|
||||||
|
<span v-if="link.icon" class="icon" v-html="link.icon"></span>
|
||||||
|
<span class="title">{{ link.title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const isStuck = ref(false)
|
||||||
|
const navigationLinks = ref([
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
title: 'Actions',
|
||||||
|
path: '/',
|
||||||
|
icon: '⚡'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logs',
|
||||||
|
title: 'Logs',
|
||||||
|
path: '/logs',
|
||||||
|
icon: '📋'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'diagnostics',
|
||||||
|
title: 'Diagnostics',
|
||||||
|
path: '/diagnostics',
|
||||||
|
icon: '🔧'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const supplementalLinks = ref([])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
|
||||||
|
function toggleStick() {
|
||||||
|
isStuck.value = !isStuck.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
isOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(path) {
|
||||||
|
return route.path === path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to add navigation links from other components
|
||||||
|
function addNavigationLink(link) {
|
||||||
|
const existingIndex = navigationLinks.value.findIndex(l => l.id === link.id)
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
navigationLinks.value[existingIndex] = { ...link }
|
||||||
|
} else {
|
||||||
|
navigationLinks.value.push({ ...link })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to add supplemental links from other components
|
||||||
|
function addSupplementalLink(link) {
|
||||||
|
const existingIndex = supplementalLinks.value.findIndex(l => l.id === link.id)
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
supplementalLinks.value[existingIndex] = { ...link }
|
||||||
|
} else {
|
||||||
|
supplementalLinks.value.push({ ...link })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to remove links
|
||||||
|
function removeNavigationLink(linkId) {
|
||||||
|
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSupplementalLink(linkId) {
|
||||||
|
supplementalLinks.value = supplementalLinks.value.filter(link => link.id !== linkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to clear all links
|
||||||
|
function clearNavigationLinks() {
|
||||||
|
navigationLinks.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSupplementalLinks() {
|
||||||
|
supplementalLinks.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get all links
|
||||||
|
function getNavigationLinks() {
|
||||||
|
return [...navigationLinks.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSupplementalLinks() {
|
||||||
|
return [...supplementalLinks.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Make the sidebar globally accessible
|
||||||
|
window.sidebar = {
|
||||||
|
get isOpen() { return isOpen.value },
|
||||||
|
set isOpen(val) { isOpen.value = val },
|
||||||
|
toggle,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
addNavigationLink,
|
||||||
|
addSupplementalLink,
|
||||||
|
removeNavigationLink,
|
||||||
|
removeSupplementalLink,
|
||||||
|
clearNavigationLinks,
|
||||||
|
clearSupplementalLinks,
|
||||||
|
getNavigationLinks,
|
||||||
|
getSupplementalLinks
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
isOpen,
|
||||||
|
navigationLinks,
|
||||||
|
supplementalLinks,
|
||||||
|
toggle,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
isActive,
|
||||||
|
addNavigationLink,
|
||||||
|
addSupplementalLink,
|
||||||
|
removeNavigationLink,
|
||||||
|
removeSupplementalLink,
|
||||||
|
clearNavigationLinks,
|
||||||
|
clearSupplementalLinks,
|
||||||
|
getNavigationLinks,
|
||||||
|
getSupplementalLinks
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mainnav {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links,
|
||||||
|
.supplemental-links {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links li,
|
||||||
|
.supplemental-links li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links a,
|
||||||
|
.supplemental-links a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links a:hover,
|
||||||
|
.supplemental-links a:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links a.active {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #007bff;
|
||||||
|
border-left-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links a.router-link-active {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #007bff;
|
||||||
|
border-left-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 1.2em;
|
||||||
|
width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplemental-links {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplemental-links a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplemental-links a:hover {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.shown {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-example">
|
||||||
|
<h3>Sidebar Management Example</h3>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="addCustomNavLink" class="btn btn-primary">
|
||||||
|
Add Custom Nav Link
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="addCustomSupplementalLink" class="btn btn-secondary">
|
||||||
|
Add Custom Supplemental Link
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="removeCustomLinks" class="btn btn-danger">
|
||||||
|
Remove Custom Links
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="toggleSidebar" class="btn btn-info">
|
||||||
|
Toggle Sidebar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<p><strong>Available Sidebar Methods:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>window.sidebar.addNavigationLink(link)</code> - Add navigation link</li>
|
||||||
|
<li><code>window.sidebar.addSupplementalLink(link)</code> - Add supplemental link</li>
|
||||||
|
<li><code>window.sidebar.removeNavigationLink(id)</code> - Remove navigation link</li>
|
||||||
|
<li><code>window.sidebar.removeSupplementalLink(id)</code> - Remove supplemental link</li>
|
||||||
|
<li><code>window.sidebar.toggle()</code> - Toggle sidebar visibility</li>
|
||||||
|
<li><code>window.sidebar.open()</code> - Open sidebar</li>
|
||||||
|
<li><code>window.sidebar.close()</code> - Close sidebar</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SidebarExample',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
customLinkId: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addCustomNavLink() {
|
||||||
|
if (window.sidebar) {
|
||||||
|
window.sidebar.addNavigationLink({
|
||||||
|
id: `custom-nav-${this.customLinkId}`,
|
||||||
|
title: `Custom Nav ${this.customLinkId}`,
|
||||||
|
path: `/custom-${this.customLinkId}`,
|
||||||
|
icon: '🔗'
|
||||||
|
})
|
||||||
|
this.customLinkId++
|
||||||
|
} else {
|
||||||
|
console.warn('Sidebar not available')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addCustomSupplementalLink() {
|
||||||
|
if (window.sidebar) {
|
||||||
|
window.sidebar.addSupplementalLink({
|
||||||
|
id: `custom-supplemental-${this.customLinkId}`,
|
||||||
|
title: `Custom Link ${this.customLinkId}`,
|
||||||
|
url: `https://example.com/custom-${this.customLinkId}`,
|
||||||
|
target: '_blank',
|
||||||
|
icon: '🔗'
|
||||||
|
})
|
||||||
|
this.customLinkId++
|
||||||
|
} else {
|
||||||
|
console.warn('Sidebar not available')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeCustomLinks() {
|
||||||
|
if (window.sidebar) {
|
||||||
|
// Remove all custom links
|
||||||
|
for (let i = 1; i < this.customLinkId; i++) {
|
||||||
|
window.sidebar.removeNavigationLink(`custom-nav-${i}`)
|
||||||
|
window.sidebar.removeSupplementalLink(`custom-supplemental-${i}`)
|
||||||
|
}
|
||||||
|
this.customLinkId = 1
|
||||||
|
} else {
|
||||||
|
console.warn('Sidebar not available')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
if (window.sidebar) {
|
||||||
|
window.sidebar.toggle()
|
||||||
|
} else {
|
||||||
|
console.warn('Sidebar not available')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar-example {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
background: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info ul {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info code {
|
||||||
|
background: #f1f3f4;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
// Import components
|
||||||
|
import App from './App.vue'
|
||||||
|
import ExecutionDialog from './ExecutionDialog.vue'
|
||||||
|
import ActionButton from './ActionButton.vue'
|
||||||
|
import ArgumentForm from './ArgumentForm.vue'
|
||||||
|
|
||||||
|
// Define routes
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('./views/DashboardRoot.vue'),
|
||||||
|
meta: { title: 'OliveTin - Dashboard' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logs',
|
||||||
|
name: 'Logs',
|
||||||
|
component: () => import('./views/LogsView.vue'),
|
||||||
|
meta: { title: 'OliveTin - Logs' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/diagnostics',
|
||||||
|
name: 'Diagnostics',
|
||||||
|
component: () => import('./views/DiagnosticsView.vue'),
|
||||||
|
meta: { title: 'OliveTin - Diagnostics' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('./views/LoginView.vue'),
|
||||||
|
meta: { title: 'OliveTin - Login' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('./views/NotFoundView.vue'),
|
||||||
|
meta: { title: 'OliveTin - Page Not Found' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create router instance
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition
|
||||||
|
} else {
|
||||||
|
return { top: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigation guard to update page title
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
if (to.meta && to.meta.title) {
|
||||||
|
document.title = to.meta.title
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigation guard for authentication (if needed)
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// Check if user is authenticated for protected routes
|
||||||
|
const isAuthenticated = window.isAuthenticated || true // Default to true for now
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||||
|
next('/login')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div v-for="dashboard in dashboards" :key="dashboard.id">
|
||||||
|
<Dashboard :dashboard="dashboard" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import Dashboard from '../Dashboard.vue'
|
||||||
|
|
||||||
|
const dashboards = ref([])
|
||||||
|
|
||||||
|
async function refreshActions() {
|
||||||
|
const ret = await window.client.getDashboardComponents();
|
||||||
|
|
||||||
|
console.log(ret.dashboards)
|
||||||
|
dashboards.value = ret.dashboards
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshActions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<template>
|
||||||
|
<div class="diagnostics-view">
|
||||||
|
<div class="diagnostics-content">
|
||||||
|
<p class="note">
|
||||||
|
<strong>Note:</strong> Diagnostics are only generated on OliveTin startup - they are not updated in real-time or
|
||||||
|
when you refresh this page.
|
||||||
|
They are intended as a "quick reference" to help you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="note">
|
||||||
|
If you are having problems with OliveTin and want to raise a support request, please don't take a screenshot or
|
||||||
|
copy text from this page,
|
||||||
|
but instead it is highly recommended to include a
|
||||||
|
<a href="https://docs.olivetin.app/sosreport.html" target="_blank">sosreport</a>
|
||||||
|
which is more detailed, and makes it easier to help you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="diagnostics-section">
|
||||||
|
<h3>SSH</h3>
|
||||||
|
<table class="diagnostics-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="10%">Found Key</td>
|
||||||
|
<td>{{ diagnostics.sshFoundKey || '?' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Found Config</td>
|
||||||
|
<td>{{ diagnostics.sshFoundConfig || '?' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="diagnostics.system" class="diagnostics-section">
|
||||||
|
<h3>System</h3>
|
||||||
|
<table class="diagnostics-table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(value, key) in diagnostics.system" :key="key">
|
||||||
|
<td width="10%">{{ formatKey(key) }}</td>
|
||||||
|
<td>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="diagnostics.network" class="diagnostics-section">
|
||||||
|
<h3>Network</h3>
|
||||||
|
<table class="diagnostics-table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(value, key) in diagnostics.network" :key="key">
|
||||||
|
<td width="10%">{{ formatKey(key) }}</td>
|
||||||
|
<td>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="diagnostics.storage" class="diagnostics-section">
|
||||||
|
<h3>Storage</h3>
|
||||||
|
<table class="diagnostics-table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(value, key) in diagnostics.storage" :key="key">
|
||||||
|
<td width="10%">{{ formatKey(key) }}</td>
|
||||||
|
<td>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="diagnostics.services" class="diagnostics-section">
|
||||||
|
<h3>Services</h3>
|
||||||
|
<table class="diagnostics-table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(value, key) in diagnostics.services" :key="key">
|
||||||
|
<td width="10%">{{ formatKey(key) }}</td>
|
||||||
|
<td>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="diagnostics.errors && diagnostics.errors.length > 0" class="diagnostics-section">
|
||||||
|
<h3>Errors</h3>
|
||||||
|
<div class="error-list">
|
||||||
|
<div v-for="(error, index) in diagnostics.errors" :key="index" class="error-item">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const diagnostics = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function fetchDiagnostics() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.client.getDiagnostics();
|
||||||
|
diagnostics.value = {
|
||||||
|
sshFoundKey: response.sshFoundKey,
|
||||||
|
sshFoundConfig: response.sshFoundConfig
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch diagnostics:', err);
|
||||||
|
diagnostics.value = {
|
||||||
|
sshFoundKey: 'Unknown',
|
||||||
|
sshFoundConfig: 'Unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatKey(key) {
|
||||||
|
return key
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/^./, str => str.toUpperCase())
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDiagnostics()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.diagnostics-view {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-section h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-table td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-table td:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<section class = "small">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-form" style="display: grid; grid-template-columns: max-content 1fr; gap: 1em;">
|
||||||
|
<h2>Login to OliveTin</h2>
|
||||||
|
|
||||||
|
<div v-if="!hasOAuth && !hasLocalLogin" class="login-disabled">
|
||||||
|
<p>This server is not configured with either OAuth, or local users, so you cannot login.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasOAuth" class="login-oauth2">
|
||||||
|
<h3>OAuth Login</h3>
|
||||||
|
<div class="oauth-providers">
|
||||||
|
<button v-for="provider in oauthProviders" :key="provider.name" class="oauth-button"
|
||||||
|
@click="loginWithOAuth(provider)">
|
||||||
|
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
||||||
|
<span class="provider-name">Login with {{ provider.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasLocalLogin" class="login-local">
|
||||||
|
<h3>Local Login</h3>
|
||||||
|
<form @submit.prevent="handleLocalLogin" class="local-login-form">
|
||||||
|
<div v-if="loginError" class="error-message">
|
||||||
|
{{ loginError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" v-model="username" type="text" name="username" autocomplete="username" required />
|
||||||
|
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input id="password" v-model="password" type="password" name="password" autocomplete="current-password"
|
||||||
|
required />
|
||||||
|
|
||||||
|
<button type="submit" :disabled="loading" class="login-button">
|
||||||
|
{{ loading ? 'Logging in...' : 'Login' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LoginView',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
loading: false,
|
||||||
|
loginError: '',
|
||||||
|
hasOAuth: false,
|
||||||
|
hasLocalLogin: false,
|
||||||
|
oauthProviders: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchLoginOptions()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchLoginOptions() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('webUiSettings.json')
|
||||||
|
const settings = await response.json()
|
||||||
|
|
||||||
|
this.hasOAuth = settings.AuthOAuth2Providers && settings.AuthOAuth2Providers.length > 0
|
||||||
|
this.hasLocalLogin = settings.AuthLocalLogin
|
||||||
|
|
||||||
|
if (this.hasOAuth) {
|
||||||
|
this.oauthProviders = settings.AuthOAuth2Providers
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch login options:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLocalLogin() {
|
||||||
|
this.loading = true
|
||||||
|
this.loginError = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Redirect to home page on successful login
|
||||||
|
this.$router.push('/')
|
||||||
|
} else {
|
||||||
|
const error = await response.text()
|
||||||
|
this.loginError = error || 'Login failed. Please check your credentials.'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err)
|
||||||
|
this.loginError = 'Network error. Please try again.'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loginWithOAuth(provider) {
|
||||||
|
// Redirect to OAuth provider
|
||||||
|
window.location.href = provider.authUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
<template>
|
||||||
|
<div class="logs-view">
|
||||||
|
<div class="toolbar">
|
||||||
|
<label class="input-with-icons">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
placeholder="Search for action name"
|
||||||
|
v-model="searchText"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
title="Clear search filter"
|
||||||
|
:disabled="!searchText"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table v-show="filteredLogs.length > 0" class="logs-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Metadata</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="log in filteredLogs"
|
||||||
|
:key="log.executionTrackingId"
|
||||||
|
class="log-row"
|
||||||
|
:title="log.actionTitle"
|
||||||
|
>
|
||||||
|
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="icon" v-html="log.actionIcon"></span>
|
||||||
|
<a href="javascript:void(0)" class="content" @click="showLogDetails(log)">
|
||||||
|
{{ log.actionTitle }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="tags">
|
||||||
|
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
|
||||||
|
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="exit-code">
|
||||||
|
<span :class="getStatusClass(log)">
|
||||||
|
{{ getStatusText(log) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div v-show="filteredLogs.length === 0" class="empty-state">
|
||||||
|
<p>There are no logs to display.</p>
|
||||||
|
<router-link to="/">Return to index</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">
|
||||||
|
<strong>Note:</strong> The server is configured to only send
|
||||||
|
<strong>{{ pageSize }}</strong> log entries at a time.
|
||||||
|
The search box at the top of this page only searches this current page of logs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LogsView',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logs: [],
|
||||||
|
searchText: '',
|
||||||
|
pageSize: '?',
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredLogs() {
|
||||||
|
if (!this.searchText) {
|
||||||
|
return this.logs
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchLower = this.searchText.toLowerCase()
|
||||||
|
return this.logs.filter(log =>
|
||||||
|
log.actionTitle.toLowerCase().includes(searchLower)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchLogs()
|
||||||
|
this.fetchPageSize()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchLogs() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await window.client.getLogs()
|
||||||
|
this.logs = response.logEntries || []
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch logs:', err)
|
||||||
|
window.showBigError('fetch-logs', 'getting logs', err, false)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPageSize() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('webUiSettings.json')
|
||||||
|
const settings = await response.json()
|
||||||
|
this.pageSize = settings.LogsPageSize || '?'
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to fetch page size:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSearch() {
|
||||||
|
// Search is handled by computed property
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
this.searchText = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTimestamp(timestamp) {
|
||||||
|
if (!timestamp) return 'Unknown'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString()
|
||||||
|
} catch (err) {
|
||||||
|
return timestamp
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusClass(log) {
|
||||||
|
if (log.timedOut) return 'status-timeout'
|
||||||
|
if (log.blocked) return 'status-blocked'
|
||||||
|
if (log.exitCode !== 0) return 'status-error'
|
||||||
|
return 'status-success'
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatusText(log) {
|
||||||
|
if (log.timedOut) return 'Timed out'
|
||||||
|
if (log.blocked) return 'Blocked'
|
||||||
|
if (log.exitCode !== 0) return `Exit code ${log.exitCode}`
|
||||||
|
return 'Success'
|
||||||
|
},
|
||||||
|
|
||||||
|
showLogDetails(log) {
|
||||||
|
// Emit event to parent or use global execution dialog
|
||||||
|
if (window.executionDialog) {
|
||||||
|
window.executionDialog.reset()
|
||||||
|
window.executionDialog.show()
|
||||||
|
window.executionDialog.fetchExecutionResult(log.executionTrackingId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logs-view {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icons input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icons button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icons button:hover:not(:disabled) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icons button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-timeout {
|
||||||
|
color: #ffc107;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-blocked {
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<div class="not-found-view">
|
||||||
|
<div class="not-found-container">
|
||||||
|
<div class="not-found-content">
|
||||||
|
<h1>404</h1>
|
||||||
|
<h2>Page Not Found</h2>
|
||||||
|
<p>The page you're looking for doesn't exist.</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<router-link to="/" class="btn btn-primary">
|
||||||
|
Go to Home
|
||||||
|
</router-link>
|
||||||
|
<button @click="goBack" class="btn btn-secondary">
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NotFoundView',
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
this.$router.go(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found-view {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content {
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content h1 {
|
||||||
|
font-size: 6rem;
|
||||||
|
margin: 0;
|
||||||
|
color: #007bff;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
@import 'femtocrank/style.css';
|
||||||
|
|
||||||
|
section.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, 180px);
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
justify-content: center;
|
||||||
|
place-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
action-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
action-button > button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.85em;
|
||||||
|
box-shadow: 0 0 .6em #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
action-button > button .icon {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
footer span {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
Components({
|
||||||
|
dirs: ['resources/vue/'],
|
||||||
|
extensions: ['vue'],
|
||||||
|
deep: true,
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/webUiSettings.json': {
|
||||||
|
target: 'http://localhost:1337',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:1337',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
version: v2
|
version: v2
|
||||||
plugins:
|
plugins:
|
||||||
- remote: buf.build/protocolbuffers/go
|
- remote: buf.build/protocolbuffers/go
|
||||||
out: ../service/gen/grpc/
|
out: ../service/gen/
|
||||||
opt: paths=source_relative
|
opt: paths=source_relative
|
||||||
|
|
||||||
- remote: buf.build/grpc/go
|
- remote: buf.build/connectrpc/go
|
||||||
out: ../service/gen/grpc/
|
out: ../service/gen/
|
||||||
opt: paths=source_relative,require_unimplemented_servers=false
|
|
||||||
|
|
||||||
- remote: buf.build/grpc-ecosystem/gateway
|
|
||||||
out: ../service/gen/grpc/
|
|
||||||
opt: paths=source_relative
|
opt: paths=source_relative
|
||||||
|
|
||||||
|
- remote: buf.build/bufbuild/es
|
||||||
|
out: ../frontend/resources/scripts/gen/
|
||||||
|
|
||||||
# - name: swagger
|
# - name: swagger
|
||||||
# out: reports/swagger
|
# out: reports/swagger
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ syntax = "proto3";
|
||||||
|
|
||||||
package olivetin.api.v1;
|
package olivetin.api.v1;
|
||||||
|
|
||||||
option go_package = "github.com/jamesread/OliveTin/gen/grpc/olivetin/api/v1;apiv1";
|
option go_package = "github.com/OliveTin/OliveTin/gen/olivetin/api/v1;apiv1";
|
||||||
|
|
||||||
import "google/api/annotations.proto";
|
|
||||||
import "google/api/httpbody.proto";
|
|
||||||
|
|
||||||
message Action {
|
message Action {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
|
|
@ -43,20 +40,12 @@ message Entity {
|
||||||
message GetDashboardComponentsResponse {
|
message GetDashboardComponentsResponse {
|
||||||
string title = 1;
|
string title = 1;
|
||||||
|
|
||||||
repeated Action actions = 2;
|
repeated Dashboard dashboards = 4;
|
||||||
repeated Entity entities = 3;
|
|
||||||
repeated DashboardComponent dashboards = 4;
|
|
||||||
|
|
||||||
string authenticated_user = 5;
|
string authenticated_user = 5;
|
||||||
string authenticated_user_provider = 6;
|
string authenticated_user_provider = 6;
|
||||||
|
|
||||||
EffectivePolicy effective_policy = 7;
|
EffectivePolicy effective_policy = 7;
|
||||||
Diagnostics diagnostics = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Diagnostics {
|
|
||||||
string SshFoundKey = 1;
|
|
||||||
string SshFoundConfig = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message EffectivePolicy {
|
message EffectivePolicy {
|
||||||
|
|
@ -66,6 +55,11 @@ message EffectivePolicy {
|
||||||
|
|
||||||
message GetDashboardComponentsRequest {}
|
message GetDashboardComponentsRequest {}
|
||||||
|
|
||||||
|
message Dashboard {
|
||||||
|
string title = 1;
|
||||||
|
repeated DashboardComponent contents = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message DashboardComponent {
|
message DashboardComponent {
|
||||||
string title = 1;
|
string title = 1;
|
||||||
string type = 2;
|
string type = 2;
|
||||||
|
|
@ -216,6 +210,19 @@ message GetReadyzResponse {
|
||||||
string status = 1;
|
string status = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message EventStreamRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
message EventStreamResponse {
|
||||||
|
oneof event {
|
||||||
|
EventEntityChanged entity_changed = 2;
|
||||||
|
EventConfigChanged config_changed = 3;
|
||||||
|
EventExecutionFinished execution_finished = 4;
|
||||||
|
EventExecutionStarted execution_started = 5;
|
||||||
|
EventOutputChunk output_chunk = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message EventOutputChunk {
|
message EventOutputChunk {
|
||||||
string execution_tracking_id = 1;
|
string execution_tracking_id = 1;
|
||||||
|
|
||||||
|
|
@ -257,117 +264,58 @@ message PasswordHashRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
message PasswordHashResponse {
|
message PasswordHashResponse {
|
||||||
|
string hash = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LogoutRequest {}
|
message LogoutRequest {}
|
||||||
|
|
||||||
|
message LogoutResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDiagnosticsRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDiagnosticsResponse {
|
||||||
|
string SshFoundKey = 1;
|
||||||
|
string SshFoundConfig = 2;
|
||||||
|
}
|
||||||
|
|
||||||
service OliveTinApiService {
|
service OliveTinApiService {
|
||||||
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
|
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/GetDashboardComponents"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc StartAction(StartActionRequest) returns (StartActionResponse) {
|
rpc StartAction(StartActionRequest) returns (StartActionResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/StartAction"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {
|
rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/StartActionAndWait"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc StartActionByGet(StartActionByGetRequest) returns (StartActionByGetResponse) {
|
rpc StartActionByGet(StartActionByGetRequest) returns (StartActionByGetResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/StartActionByGet/{action_id}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc StartActionByGetAndWait(StartActionByGetAndWaitRequest) returns (StartActionByGetAndWaitResponse) {
|
rpc StartActionByGetAndWait(StartActionByGetAndWaitRequest) returns (StartActionByGetAndWaitResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/StartActionByGetAndWait/{action_id}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc KillAction(KillActionRequest) returns (KillActionResponse) {
|
rpc KillAction(KillActionRequest) returns (KillActionResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/KillAction"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
|
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/ExecutionStatus"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
|
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/GetLogs"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {
|
rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/ValidateArgumentType"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse) {
|
rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/WhoAmI"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc SosReport(SosReportRequest) returns (google.api.HttpBody) {
|
rpc SosReport(SosReportRequest) returns (SosReportResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/sosreport"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc DumpVars(DumpVarsRequest) returns (DumpVarsResponse) {
|
rpc DumpVars(DumpVarsRequest) returns (DumpVarsResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/DumpVars"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc DumpPublicIdActionMap(DumpPublicIdActionMapRequest) returns (DumpPublicIdActionMapResponse) {
|
rpc DumpPublicIdActionMap(DumpPublicIdActionMapRequest) returns (DumpPublicIdActionMapResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/DumpActionMap"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc GetReadyz(GetReadyzRequest) returns (GetReadyzResponse) {
|
rpc GetReadyz(GetReadyzRequest) returns (GetReadyzResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/readyz"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc LocalUserLogin(LocalUserLoginRequest) returns (LocalUserLoginResponse) {
|
rpc LocalUserLogin(LocalUserLoginRequest) returns (LocalUserLoginResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/LocalUserLogin"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc PasswordHash(PasswordHashRequest) returns (google.api.HttpBody) {
|
rpc PasswordHash(PasswordHashRequest) returns (PasswordHashResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
post: "/api/PasswordHash"
|
|
||||||
body: "*"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc Logout(LogoutRequest) returns (google.api.HttpBody) {
|
rpc Logout(LogoutRequest) returns (LogoutResponse) {}
|
||||||
option (google.api.http) = {
|
|
||||||
get: "/api/Logout"
|
rpc EventStream(EventStreamRequest) returns (stream EventStreamResponse) {}
|
||||||
};
|
|
||||||
}
|
rpc GetDiagnostics(GetDiagnosticsRequest) returns (GetDiagnosticsResponse) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,728 +0,0 @@
|
||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
|
||||||
// - protoc (unknown)
|
|
||||||
// source: olivetin/api/v1/olivetin.proto
|
|
||||||
|
|
||||||
package apiv1
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
httpbody "google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
grpc "google.golang.org/grpc"
|
|
||||||
codes "google.golang.org/grpc/codes"
|
|
||||||
status "google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is a compile-time assertion to ensure that this generated file
|
|
||||||
// is compatible with the grpc package it is being compiled against.
|
|
||||||
// Requires gRPC-Go v1.64.0 or later.
|
|
||||||
const _ = grpc.SupportPackageIsVersion9
|
|
||||||
|
|
||||||
const (
|
|
||||||
OliveTinApiService_GetDashboardComponents_FullMethodName = "/olivetin.api.v1.OliveTinApiService/GetDashboardComponents"
|
|
||||||
OliveTinApiService_StartAction_FullMethodName = "/olivetin.api.v1.OliveTinApiService/StartAction"
|
|
||||||
OliveTinApiService_StartActionAndWait_FullMethodName = "/olivetin.api.v1.OliveTinApiService/StartActionAndWait"
|
|
||||||
OliveTinApiService_StartActionByGet_FullMethodName = "/olivetin.api.v1.OliveTinApiService/StartActionByGet"
|
|
||||||
OliveTinApiService_StartActionByGetAndWait_FullMethodName = "/olivetin.api.v1.OliveTinApiService/StartActionByGetAndWait"
|
|
||||||
OliveTinApiService_KillAction_FullMethodName = "/olivetin.api.v1.OliveTinApiService/KillAction"
|
|
||||||
OliveTinApiService_ExecutionStatus_FullMethodName = "/olivetin.api.v1.OliveTinApiService/ExecutionStatus"
|
|
||||||
OliveTinApiService_GetLogs_FullMethodName = "/olivetin.api.v1.OliveTinApiService/GetLogs"
|
|
||||||
OliveTinApiService_ValidateArgumentType_FullMethodName = "/olivetin.api.v1.OliveTinApiService/ValidateArgumentType"
|
|
||||||
OliveTinApiService_WhoAmI_FullMethodName = "/olivetin.api.v1.OliveTinApiService/WhoAmI"
|
|
||||||
OliveTinApiService_SosReport_FullMethodName = "/olivetin.api.v1.OliveTinApiService/SosReport"
|
|
||||||
OliveTinApiService_DumpVars_FullMethodName = "/olivetin.api.v1.OliveTinApiService/DumpVars"
|
|
||||||
OliveTinApiService_DumpPublicIdActionMap_FullMethodName = "/olivetin.api.v1.OliveTinApiService/DumpPublicIdActionMap"
|
|
||||||
OliveTinApiService_GetReadyz_FullMethodName = "/olivetin.api.v1.OliveTinApiService/GetReadyz"
|
|
||||||
OliveTinApiService_LocalUserLogin_FullMethodName = "/olivetin.api.v1.OliveTinApiService/LocalUserLogin"
|
|
||||||
OliveTinApiService_PasswordHash_FullMethodName = "/olivetin.api.v1.OliveTinApiService/PasswordHash"
|
|
||||||
OliveTinApiService_Logout_FullMethodName = "/olivetin.api.v1.OliveTinApiService/Logout"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OliveTinApiServiceClient is the client API for OliveTinApiService service.
|
|
||||||
//
|
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
|
||||||
type OliveTinApiServiceClient interface {
|
|
||||||
GetDashboardComponents(ctx context.Context, in *GetDashboardComponentsRequest, opts ...grpc.CallOption) (*GetDashboardComponentsResponse, error)
|
|
||||||
StartAction(ctx context.Context, in *StartActionRequest, opts ...grpc.CallOption) (*StartActionResponse, error)
|
|
||||||
StartActionAndWait(ctx context.Context, in *StartActionAndWaitRequest, opts ...grpc.CallOption) (*StartActionAndWaitResponse, error)
|
|
||||||
StartActionByGet(ctx context.Context, in *StartActionByGetRequest, opts ...grpc.CallOption) (*StartActionByGetResponse, error)
|
|
||||||
StartActionByGetAndWait(ctx context.Context, in *StartActionByGetAndWaitRequest, opts ...grpc.CallOption) (*StartActionByGetAndWaitResponse, error)
|
|
||||||
KillAction(ctx context.Context, in *KillActionRequest, opts ...grpc.CallOption) (*KillActionResponse, error)
|
|
||||||
ExecutionStatus(ctx context.Context, in *ExecutionStatusRequest, opts ...grpc.CallOption) (*ExecutionStatusResponse, error)
|
|
||||||
GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error)
|
|
||||||
ValidateArgumentType(ctx context.Context, in *ValidateArgumentTypeRequest, opts ...grpc.CallOption) (*ValidateArgumentTypeResponse, error)
|
|
||||||
WhoAmI(ctx context.Context, in *WhoAmIRequest, opts ...grpc.CallOption) (*WhoAmIResponse, error)
|
|
||||||
SosReport(ctx context.Context, in *SosReportRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
|
||||||
DumpVars(ctx context.Context, in *DumpVarsRequest, opts ...grpc.CallOption) (*DumpVarsResponse, error)
|
|
||||||
DumpPublicIdActionMap(ctx context.Context, in *DumpPublicIdActionMapRequest, opts ...grpc.CallOption) (*DumpPublicIdActionMapResponse, error)
|
|
||||||
GetReadyz(ctx context.Context, in *GetReadyzRequest, opts ...grpc.CallOption) (*GetReadyzResponse, error)
|
|
||||||
LocalUserLogin(ctx context.Context, in *LocalUserLoginRequest, opts ...grpc.CallOption) (*LocalUserLoginResponse, error)
|
|
||||||
PasswordHash(ctx context.Context, in *PasswordHashRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
|
||||||
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type oliveTinApiServiceClient struct {
|
|
||||||
cc grpc.ClientConnInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOliveTinApiServiceClient(cc grpc.ClientConnInterface) OliveTinApiServiceClient {
|
|
||||||
return &oliveTinApiServiceClient{cc}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) GetDashboardComponents(ctx context.Context, in *GetDashboardComponentsRequest, opts ...grpc.CallOption) (*GetDashboardComponentsResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(GetDashboardComponentsResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_GetDashboardComponents_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) StartAction(ctx context.Context, in *StartActionRequest, opts ...grpc.CallOption) (*StartActionResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(StartActionResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_StartAction_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) StartActionAndWait(ctx context.Context, in *StartActionAndWaitRequest, opts ...grpc.CallOption) (*StartActionAndWaitResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(StartActionAndWaitResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_StartActionAndWait_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) StartActionByGet(ctx context.Context, in *StartActionByGetRequest, opts ...grpc.CallOption) (*StartActionByGetResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(StartActionByGetResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_StartActionByGet_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) StartActionByGetAndWait(ctx context.Context, in *StartActionByGetAndWaitRequest, opts ...grpc.CallOption) (*StartActionByGetAndWaitResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(StartActionByGetAndWaitResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_StartActionByGetAndWait_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) KillAction(ctx context.Context, in *KillActionRequest, opts ...grpc.CallOption) (*KillActionResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(KillActionResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_KillAction_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) ExecutionStatus(ctx context.Context, in *ExecutionStatusRequest, opts ...grpc.CallOption) (*ExecutionStatusResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(ExecutionStatusResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_ExecutionStatus_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(GetLogsResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_GetLogs_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) ValidateArgumentType(ctx context.Context, in *ValidateArgumentTypeRequest, opts ...grpc.CallOption) (*ValidateArgumentTypeResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(ValidateArgumentTypeResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_ValidateArgumentType_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) WhoAmI(ctx context.Context, in *WhoAmIRequest, opts ...grpc.CallOption) (*WhoAmIResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(WhoAmIResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_WhoAmI_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) SosReport(ctx context.Context, in *SosReportRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(httpbody.HttpBody)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_SosReport_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) DumpVars(ctx context.Context, in *DumpVarsRequest, opts ...grpc.CallOption) (*DumpVarsResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(DumpVarsResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_DumpVars_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) DumpPublicIdActionMap(ctx context.Context, in *DumpPublicIdActionMapRequest, opts ...grpc.CallOption) (*DumpPublicIdActionMapResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(DumpPublicIdActionMapResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_DumpPublicIdActionMap_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) GetReadyz(ctx context.Context, in *GetReadyzRequest, opts ...grpc.CallOption) (*GetReadyzResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(GetReadyzResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_GetReadyz_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) LocalUserLogin(ctx context.Context, in *LocalUserLoginRequest, opts ...grpc.CallOption) (*LocalUserLoginResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(LocalUserLoginResponse)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_LocalUserLogin_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) PasswordHash(ctx context.Context, in *PasswordHashRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(httpbody.HttpBody)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_PasswordHash_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *oliveTinApiServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(httpbody.HttpBody)
|
|
||||||
err := c.cc.Invoke(ctx, OliveTinApiService_Logout_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OliveTinApiServiceServer is the server API for OliveTinApiService service.
|
|
||||||
// All implementations should embed UnimplementedOliveTinApiServiceServer
|
|
||||||
// for forward compatibility.
|
|
||||||
type OliveTinApiServiceServer interface {
|
|
||||||
GetDashboardComponents(context.Context, *GetDashboardComponentsRequest) (*GetDashboardComponentsResponse, error)
|
|
||||||
StartAction(context.Context, *StartActionRequest) (*StartActionResponse, error)
|
|
||||||
StartActionAndWait(context.Context, *StartActionAndWaitRequest) (*StartActionAndWaitResponse, error)
|
|
||||||
StartActionByGet(context.Context, *StartActionByGetRequest) (*StartActionByGetResponse, error)
|
|
||||||
StartActionByGetAndWait(context.Context, *StartActionByGetAndWaitRequest) (*StartActionByGetAndWaitResponse, error)
|
|
||||||
KillAction(context.Context, *KillActionRequest) (*KillActionResponse, error)
|
|
||||||
ExecutionStatus(context.Context, *ExecutionStatusRequest) (*ExecutionStatusResponse, error)
|
|
||||||
GetLogs(context.Context, *GetLogsRequest) (*GetLogsResponse, error)
|
|
||||||
ValidateArgumentType(context.Context, *ValidateArgumentTypeRequest) (*ValidateArgumentTypeResponse, error)
|
|
||||||
WhoAmI(context.Context, *WhoAmIRequest) (*WhoAmIResponse, error)
|
|
||||||
SosReport(context.Context, *SosReportRequest) (*httpbody.HttpBody, error)
|
|
||||||
DumpVars(context.Context, *DumpVarsRequest) (*DumpVarsResponse, error)
|
|
||||||
DumpPublicIdActionMap(context.Context, *DumpPublicIdActionMapRequest) (*DumpPublicIdActionMapResponse, error)
|
|
||||||
GetReadyz(context.Context, *GetReadyzRequest) (*GetReadyzResponse, error)
|
|
||||||
LocalUserLogin(context.Context, *LocalUserLoginRequest) (*LocalUserLoginResponse, error)
|
|
||||||
PasswordHash(context.Context, *PasswordHashRequest) (*httpbody.HttpBody, error)
|
|
||||||
Logout(context.Context, *LogoutRequest) (*httpbody.HttpBody, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnimplementedOliveTinApiServiceServer should be embedded to have
|
|
||||||
// forward compatible implementations.
|
|
||||||
//
|
|
||||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
|
||||||
// pointer dereference when methods are called.
|
|
||||||
type UnimplementedOliveTinApiServiceServer struct{}
|
|
||||||
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) GetDashboardComponents(context.Context, *GetDashboardComponentsRequest) (*GetDashboardComponentsResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetDashboardComponents not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) StartAction(context.Context, *StartActionRequest) (*StartActionResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StartAction not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) StartActionAndWait(context.Context, *StartActionAndWaitRequest) (*StartActionAndWaitResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StartActionAndWait not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) StartActionByGet(context.Context, *StartActionByGetRequest) (*StartActionByGetResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StartActionByGet not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) StartActionByGetAndWait(context.Context, *StartActionByGetAndWaitRequest) (*StartActionByGetAndWaitResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StartActionByGetAndWait not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) KillAction(context.Context, *KillActionRequest) (*KillActionResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method KillAction not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) ExecutionStatus(context.Context, *ExecutionStatusRequest) (*ExecutionStatusResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ExecutionStatus not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) GetLogs(context.Context, *GetLogsRequest) (*GetLogsResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) ValidateArgumentType(context.Context, *ValidateArgumentTypeRequest) (*ValidateArgumentTypeResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ValidateArgumentType not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) WhoAmI(context.Context, *WhoAmIRequest) (*WhoAmIResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method WhoAmI not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) SosReport(context.Context, *SosReportRequest) (*httpbody.HttpBody, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SosReport not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) DumpVars(context.Context, *DumpVarsRequest) (*DumpVarsResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method DumpVars not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) DumpPublicIdActionMap(context.Context, *DumpPublicIdActionMapRequest) (*DumpPublicIdActionMapResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method DumpPublicIdActionMap not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) GetReadyz(context.Context, *GetReadyzRequest) (*GetReadyzResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetReadyz not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) LocalUserLogin(context.Context, *LocalUserLoginRequest) (*LocalUserLoginResponse, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method LocalUserLogin not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) PasswordHash(context.Context, *PasswordHashRequest) (*httpbody.HttpBody, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method PasswordHash not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) Logout(context.Context, *LogoutRequest) (*httpbody.HttpBody, error) {
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedOliveTinApiServiceServer) testEmbeddedByValue() {}
|
|
||||||
|
|
||||||
// UnsafeOliveTinApiServiceServer may be embedded to opt out of forward compatibility for this service.
|
|
||||||
// Use of this interface is not recommended, as added methods to OliveTinApiServiceServer will
|
|
||||||
// result in compilation errors.
|
|
||||||
type UnsafeOliveTinApiServiceServer interface {
|
|
||||||
mustEmbedUnimplementedOliveTinApiServiceServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterOliveTinApiServiceServer(s grpc.ServiceRegistrar, srv OliveTinApiServiceServer) {
|
|
||||||
// If the following call pancis, it indicates UnimplementedOliveTinApiServiceServer was
|
|
||||||
// embedded by pointer and is nil. This will cause panics if an
|
|
||||||
// unimplemented method is ever invoked, so we test this at initialization
|
|
||||||
// time to prevent it from happening at runtime later due to I/O.
|
|
||||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
|
||||||
t.testEmbeddedByValue()
|
|
||||||
}
|
|
||||||
s.RegisterService(&OliveTinApiService_ServiceDesc, srv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_GetDashboardComponents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(GetDashboardComponentsRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).GetDashboardComponents(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_GetDashboardComponents_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).GetDashboardComponents(ctx, req.(*GetDashboardComponentsRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_StartAction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(StartActionRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartAction(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_StartAction_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartAction(ctx, req.(*StartActionRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_StartActionAndWait_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(StartActionAndWaitRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartActionAndWait(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_StartActionAndWait_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartActionAndWait(ctx, req.(*StartActionAndWaitRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_StartActionByGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(StartActionByGetRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartActionByGet(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_StartActionByGet_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartActionByGet(ctx, req.(*StartActionByGetRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_StartActionByGetAndWait_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(StartActionByGetAndWaitRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartActionByGetAndWait(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_StartActionByGetAndWait_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).StartActionByGetAndWait(ctx, req.(*StartActionByGetAndWaitRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_KillAction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(KillActionRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).KillAction(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_KillAction_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).KillAction(ctx, req.(*KillActionRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_ExecutionStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(ExecutionStatusRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).ExecutionStatus(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_ExecutionStatus_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).ExecutionStatus(ctx, req.(*ExecutionStatusRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(GetLogsRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).GetLogs(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_GetLogs_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).GetLogs(ctx, req.(*GetLogsRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_ValidateArgumentType_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(ValidateArgumentTypeRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).ValidateArgumentType(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_ValidateArgumentType_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).ValidateArgumentType(ctx, req.(*ValidateArgumentTypeRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_WhoAmI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(WhoAmIRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).WhoAmI(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_WhoAmI_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).WhoAmI(ctx, req.(*WhoAmIRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_SosReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(SosReportRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).SosReport(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_SosReport_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).SosReport(ctx, req.(*SosReportRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_DumpVars_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(DumpVarsRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).DumpVars(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_DumpVars_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).DumpVars(ctx, req.(*DumpVarsRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_DumpPublicIdActionMap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(DumpPublicIdActionMapRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).DumpPublicIdActionMap(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_DumpPublicIdActionMap_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).DumpPublicIdActionMap(ctx, req.(*DumpPublicIdActionMapRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_GetReadyz_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(GetReadyzRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).GetReadyz(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_GetReadyz_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).GetReadyz(ctx, req.(*GetReadyzRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_LocalUserLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(LocalUserLoginRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).LocalUserLogin(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_LocalUserLogin_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).LocalUserLogin(ctx, req.(*LocalUserLoginRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_PasswordHash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(PasswordHashRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).PasswordHash(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_PasswordHash_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).PasswordHash(ctx, req.(*PasswordHashRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _OliveTinApiService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(LogoutRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(OliveTinApiServiceServer).Logout(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: OliveTinApiService_Logout_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(OliveTinApiServiceServer).Logout(ctx, req.(*LogoutRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OliveTinApiService_ServiceDesc is the grpc.ServiceDesc for OliveTinApiService service.
|
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
|
||||||
// and not to be introspected or modified (even as a copy)
|
|
||||||
var OliveTinApiService_ServiceDesc = grpc.ServiceDesc{
|
|
||||||
ServiceName: "olivetin.api.v1.OliveTinApiService",
|
|
||||||
HandlerType: (*OliveTinApiServiceServer)(nil),
|
|
||||||
Methods: []grpc.MethodDesc{
|
|
||||||
{
|
|
||||||
MethodName: "GetDashboardComponents",
|
|
||||||
Handler: _OliveTinApiService_GetDashboardComponents_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "StartAction",
|
|
||||||
Handler: _OliveTinApiService_StartAction_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "StartActionAndWait",
|
|
||||||
Handler: _OliveTinApiService_StartActionAndWait_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "StartActionByGet",
|
|
||||||
Handler: _OliveTinApiService_StartActionByGet_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "StartActionByGetAndWait",
|
|
||||||
Handler: _OliveTinApiService_StartActionByGetAndWait_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "KillAction",
|
|
||||||
Handler: _OliveTinApiService_KillAction_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "ExecutionStatus",
|
|
||||||
Handler: _OliveTinApiService_ExecutionStatus_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "GetLogs",
|
|
||||||
Handler: _OliveTinApiService_GetLogs_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "ValidateArgumentType",
|
|
||||||
Handler: _OliveTinApiService_ValidateArgumentType_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "WhoAmI",
|
|
||||||
Handler: _OliveTinApiService_WhoAmI_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "SosReport",
|
|
||||||
Handler: _OliveTinApiService_SosReport_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "DumpVars",
|
|
||||||
Handler: _OliveTinApiService_DumpVars_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "DumpPublicIdActionMap",
|
|
||||||
Handler: _OliveTinApiService_DumpPublicIdActionMap_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "GetReadyz",
|
|
||||||
Handler: _OliveTinApiService_GetReadyz_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "LocalUserLogin",
|
|
||||||
Handler: _OliveTinApiService_LocalUserLogin_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "PasswordHash",
|
|
||||||
Handler: _OliveTinApiService_PasswordHash_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "Logout",
|
|
||||||
Handler: _OliveTinApiService_Logout_Handler,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Streams: []grpc.StreamDesc{},
|
|
||||||
Metadata: "olivetin/api/v1/olivetin.proto",
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,631 @@
|
||||||
|
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// Source: olivetin/api/v1/olivetin.proto
|
||||||
|
|
||||||
|
package apiv1connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
connect "connectrpc.com/connect"
|
||||||
|
context "context"
|
||||||
|
errors "errors"
|
||||||
|
v1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
|
http "net/http"
|
||||||
|
strings "strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||||
|
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||||
|
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||||
|
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||||
|
// version compiled into your binary.
|
||||||
|
const _ = connect.IsAtLeastVersion1_13_0
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OliveTinApiServiceName is the fully-qualified name of the OliveTinApiService service.
|
||||||
|
OliveTinApiServiceName = "olivetin.api.v1.OliveTinApiService"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||||
|
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||||
|
//
|
||||||
|
// Note that these are different from the fully-qualified method names used by
|
||||||
|
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||||
|
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||||
|
// period.
|
||||||
|
const (
|
||||||
|
// OliveTinApiServiceGetDashboardComponentsProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's GetDashboardComponents RPC.
|
||||||
|
OliveTinApiServiceGetDashboardComponentsProcedure = "/olivetin.api.v1.OliveTinApiService/GetDashboardComponents"
|
||||||
|
// OliveTinApiServiceStartActionProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// StartAction RPC.
|
||||||
|
OliveTinApiServiceStartActionProcedure = "/olivetin.api.v1.OliveTinApiService/StartAction"
|
||||||
|
// OliveTinApiServiceStartActionAndWaitProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's StartActionAndWait RPC.
|
||||||
|
OliveTinApiServiceStartActionAndWaitProcedure = "/olivetin.api.v1.OliveTinApiService/StartActionAndWait"
|
||||||
|
// OliveTinApiServiceStartActionByGetProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's StartActionByGet RPC.
|
||||||
|
OliveTinApiServiceStartActionByGetProcedure = "/olivetin.api.v1.OliveTinApiService/StartActionByGet"
|
||||||
|
// OliveTinApiServiceStartActionByGetAndWaitProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's StartActionByGetAndWait RPC.
|
||||||
|
OliveTinApiServiceStartActionByGetAndWaitProcedure = "/olivetin.api.v1.OliveTinApiService/StartActionByGetAndWait"
|
||||||
|
// OliveTinApiServiceKillActionProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// KillAction RPC.
|
||||||
|
OliveTinApiServiceKillActionProcedure = "/olivetin.api.v1.OliveTinApiService/KillAction"
|
||||||
|
// OliveTinApiServiceExecutionStatusProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's ExecutionStatus RPC.
|
||||||
|
OliveTinApiServiceExecutionStatusProcedure = "/olivetin.api.v1.OliveTinApiService/ExecutionStatus"
|
||||||
|
// OliveTinApiServiceGetLogsProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// GetLogs RPC.
|
||||||
|
OliveTinApiServiceGetLogsProcedure = "/olivetin.api.v1.OliveTinApiService/GetLogs"
|
||||||
|
// OliveTinApiServiceValidateArgumentTypeProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's ValidateArgumentType RPC.
|
||||||
|
OliveTinApiServiceValidateArgumentTypeProcedure = "/olivetin.api.v1.OliveTinApiService/ValidateArgumentType"
|
||||||
|
// OliveTinApiServiceWhoAmIProcedure is the fully-qualified name of the OliveTinApiService's WhoAmI
|
||||||
|
// RPC.
|
||||||
|
OliveTinApiServiceWhoAmIProcedure = "/olivetin.api.v1.OliveTinApiService/WhoAmI"
|
||||||
|
// OliveTinApiServiceSosReportProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// SosReport RPC.
|
||||||
|
OliveTinApiServiceSosReportProcedure = "/olivetin.api.v1.OliveTinApiService/SosReport"
|
||||||
|
// OliveTinApiServiceDumpVarsProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// DumpVars RPC.
|
||||||
|
OliveTinApiServiceDumpVarsProcedure = "/olivetin.api.v1.OliveTinApiService/DumpVars"
|
||||||
|
// OliveTinApiServiceDumpPublicIdActionMapProcedure is the fully-qualified name of the
|
||||||
|
// OliveTinApiService's DumpPublicIdActionMap RPC.
|
||||||
|
OliveTinApiServiceDumpPublicIdActionMapProcedure = "/olivetin.api.v1.OliveTinApiService/DumpPublicIdActionMap"
|
||||||
|
// OliveTinApiServiceGetReadyzProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// GetReadyz RPC.
|
||||||
|
OliveTinApiServiceGetReadyzProcedure = "/olivetin.api.v1.OliveTinApiService/GetReadyz"
|
||||||
|
// OliveTinApiServiceLocalUserLoginProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// LocalUserLogin RPC.
|
||||||
|
OliveTinApiServiceLocalUserLoginProcedure = "/olivetin.api.v1.OliveTinApiService/LocalUserLogin"
|
||||||
|
// OliveTinApiServicePasswordHashProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// PasswordHash RPC.
|
||||||
|
OliveTinApiServicePasswordHashProcedure = "/olivetin.api.v1.OliveTinApiService/PasswordHash"
|
||||||
|
// OliveTinApiServiceLogoutProcedure is the fully-qualified name of the OliveTinApiService's Logout
|
||||||
|
// RPC.
|
||||||
|
OliveTinApiServiceLogoutProcedure = "/olivetin.api.v1.OliveTinApiService/Logout"
|
||||||
|
// OliveTinApiServiceEventStreamProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// EventStream RPC.
|
||||||
|
OliveTinApiServiceEventStreamProcedure = "/olivetin.api.v1.OliveTinApiService/EventStream"
|
||||||
|
// OliveTinApiServiceGetDiagnosticsProcedure is the fully-qualified name of the OliveTinApiService's
|
||||||
|
// GetDiagnostics RPC.
|
||||||
|
OliveTinApiServiceGetDiagnosticsProcedure = "/olivetin.api.v1.OliveTinApiService/GetDiagnostics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OliveTinApiServiceClient is a client for the olivetin.api.v1.OliveTinApiService service.
|
||||||
|
type OliveTinApiServiceClient interface {
|
||||||
|
GetDashboardComponents(context.Context, *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error)
|
||||||
|
StartAction(context.Context, *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error)
|
||||||
|
StartActionAndWait(context.Context, *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error)
|
||||||
|
StartActionByGet(context.Context, *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error)
|
||||||
|
StartActionByGetAndWait(context.Context, *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error)
|
||||||
|
KillAction(context.Context, *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error)
|
||||||
|
ExecutionStatus(context.Context, *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error)
|
||||||
|
GetLogs(context.Context, *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error)
|
||||||
|
ValidateArgumentType(context.Context, *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error)
|
||||||
|
WhoAmI(context.Context, *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error)
|
||||||
|
SosReport(context.Context, *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error)
|
||||||
|
DumpVars(context.Context, *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error)
|
||||||
|
DumpPublicIdActionMap(context.Context, *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error)
|
||||||
|
GetReadyz(context.Context, *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error)
|
||||||
|
LocalUserLogin(context.Context, *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error)
|
||||||
|
PasswordHash(context.Context, *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error)
|
||||||
|
Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error)
|
||||||
|
EventStream(context.Context, *connect.Request[v1.EventStreamRequest]) (*connect.ServerStreamForClient[v1.EventStreamResponse], error)
|
||||||
|
GetDiagnostics(context.Context, *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOliveTinApiServiceClient constructs a client for the olivetin.api.v1.OliveTinApiService
|
||||||
|
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
|
||||||
|
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
|
||||||
|
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
|
||||||
|
//
|
||||||
|
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||||
|
// http://api.acme.com or https://acme.com/grpc).
|
||||||
|
func NewOliveTinApiServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) OliveTinApiServiceClient {
|
||||||
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
|
oliveTinApiServiceMethods := v1.File_olivetin_api_v1_olivetin_proto.Services().ByName("OliveTinApiService").Methods()
|
||||||
|
return &oliveTinApiServiceClient{
|
||||||
|
getDashboardComponents: connect.NewClient[v1.GetDashboardComponentsRequest, v1.GetDashboardComponentsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceGetDashboardComponentsProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDashboardComponents")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
startAction: connect.NewClient[v1.StartActionRequest, v1.StartActionResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceStartActionProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartAction")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
startActionAndWait: connect.NewClient[v1.StartActionAndWaitRequest, v1.StartActionAndWaitResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceStartActionAndWaitProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionAndWait")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
startActionByGet: connect.NewClient[v1.StartActionByGetRequest, v1.StartActionByGetResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceStartActionByGetProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGet")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
startActionByGetAndWait: connect.NewClient[v1.StartActionByGetAndWaitRequest, v1.StartActionByGetAndWaitResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceStartActionByGetAndWaitProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGetAndWait")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
killAction: connect.NewClient[v1.KillActionRequest, v1.KillActionResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceKillActionProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("KillAction")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
executionStatus: connect.NewClient[v1.ExecutionStatusRequest, v1.ExecutionStatusResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceExecutionStatusProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("ExecutionStatus")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
getLogs: connect.NewClient[v1.GetLogsRequest, v1.GetLogsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceGetLogsProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetLogs")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
validateArgumentType: connect.NewClient[v1.ValidateArgumentTypeRequest, v1.ValidateArgumentTypeResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceValidateArgumentTypeProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("ValidateArgumentType")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
whoAmI: connect.NewClient[v1.WhoAmIRequest, v1.WhoAmIResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceWhoAmIProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("WhoAmI")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
sosReport: connect.NewClient[v1.SosReportRequest, v1.SosReportResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceSosReportProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("SosReport")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
dumpVars: connect.NewClient[v1.DumpVarsRequest, v1.DumpVarsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceDumpVarsProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpVars")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
dumpPublicIdActionMap: connect.NewClient[v1.DumpPublicIdActionMapRequest, v1.DumpPublicIdActionMapResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceDumpPublicIdActionMapProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpPublicIdActionMap")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
getReadyz: connect.NewClient[v1.GetReadyzRequest, v1.GetReadyzResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceGetReadyzProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetReadyz")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
localUserLogin: connect.NewClient[v1.LocalUserLoginRequest, v1.LocalUserLoginResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceLocalUserLoginProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("LocalUserLogin")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
passwordHash: connect.NewClient[v1.PasswordHashRequest, v1.PasswordHashResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServicePasswordHashProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("PasswordHash")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
logout: connect.NewClient[v1.LogoutRequest, v1.LogoutResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceLogoutProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("Logout")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
eventStream: connect.NewClient[v1.EventStreamRequest, v1.EventStreamResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceEventStreamProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("EventStream")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
getDiagnostics: connect.NewClient[v1.GetDiagnosticsRequest, v1.GetDiagnosticsResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+OliveTinApiServiceGetDiagnosticsProcedure,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDiagnostics")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// oliveTinApiServiceClient implements OliveTinApiServiceClient.
|
||||||
|
type oliveTinApiServiceClient struct {
|
||||||
|
getDashboardComponents *connect.Client[v1.GetDashboardComponentsRequest, v1.GetDashboardComponentsResponse]
|
||||||
|
startAction *connect.Client[v1.StartActionRequest, v1.StartActionResponse]
|
||||||
|
startActionAndWait *connect.Client[v1.StartActionAndWaitRequest, v1.StartActionAndWaitResponse]
|
||||||
|
startActionByGet *connect.Client[v1.StartActionByGetRequest, v1.StartActionByGetResponse]
|
||||||
|
startActionByGetAndWait *connect.Client[v1.StartActionByGetAndWaitRequest, v1.StartActionByGetAndWaitResponse]
|
||||||
|
killAction *connect.Client[v1.KillActionRequest, v1.KillActionResponse]
|
||||||
|
executionStatus *connect.Client[v1.ExecutionStatusRequest, v1.ExecutionStatusResponse]
|
||||||
|
getLogs *connect.Client[v1.GetLogsRequest, v1.GetLogsResponse]
|
||||||
|
validateArgumentType *connect.Client[v1.ValidateArgumentTypeRequest, v1.ValidateArgumentTypeResponse]
|
||||||
|
whoAmI *connect.Client[v1.WhoAmIRequest, v1.WhoAmIResponse]
|
||||||
|
sosReport *connect.Client[v1.SosReportRequest, v1.SosReportResponse]
|
||||||
|
dumpVars *connect.Client[v1.DumpVarsRequest, v1.DumpVarsResponse]
|
||||||
|
dumpPublicIdActionMap *connect.Client[v1.DumpPublicIdActionMapRequest, v1.DumpPublicIdActionMapResponse]
|
||||||
|
getReadyz *connect.Client[v1.GetReadyzRequest, v1.GetReadyzResponse]
|
||||||
|
localUserLogin *connect.Client[v1.LocalUserLoginRequest, v1.LocalUserLoginResponse]
|
||||||
|
passwordHash *connect.Client[v1.PasswordHashRequest, v1.PasswordHashResponse]
|
||||||
|
logout *connect.Client[v1.LogoutRequest, v1.LogoutResponse]
|
||||||
|
eventStream *connect.Client[v1.EventStreamRequest, v1.EventStreamResponse]
|
||||||
|
getDiagnostics *connect.Client[v1.GetDiagnosticsRequest, v1.GetDiagnosticsResponse]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardComponents calls olivetin.api.v1.OliveTinApiService.GetDashboardComponents.
|
||||||
|
func (c *oliveTinApiServiceClient) GetDashboardComponents(ctx context.Context, req *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error) {
|
||||||
|
return c.getDashboardComponents.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAction calls olivetin.api.v1.OliveTinApiService.StartAction.
|
||||||
|
func (c *oliveTinApiServiceClient) StartAction(ctx context.Context, req *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error) {
|
||||||
|
return c.startAction.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartActionAndWait calls olivetin.api.v1.OliveTinApiService.StartActionAndWait.
|
||||||
|
func (c *oliveTinApiServiceClient) StartActionAndWait(ctx context.Context, req *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error) {
|
||||||
|
return c.startActionAndWait.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartActionByGet calls olivetin.api.v1.OliveTinApiService.StartActionByGet.
|
||||||
|
func (c *oliveTinApiServiceClient) StartActionByGet(ctx context.Context, req *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error) {
|
||||||
|
return c.startActionByGet.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartActionByGetAndWait calls olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait.
|
||||||
|
func (c *oliveTinApiServiceClient) StartActionByGetAndWait(ctx context.Context, req *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error) {
|
||||||
|
return c.startActionByGetAndWait.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KillAction calls olivetin.api.v1.OliveTinApiService.KillAction.
|
||||||
|
func (c *oliveTinApiServiceClient) KillAction(ctx context.Context, req *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error) {
|
||||||
|
return c.killAction.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionStatus calls olivetin.api.v1.OliveTinApiService.ExecutionStatus.
|
||||||
|
func (c *oliveTinApiServiceClient) ExecutionStatus(ctx context.Context, req *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error) {
|
||||||
|
return c.executionStatus.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogs calls olivetin.api.v1.OliveTinApiService.GetLogs.
|
||||||
|
func (c *oliveTinApiServiceClient) GetLogs(ctx context.Context, req *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error) {
|
||||||
|
return c.getLogs.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateArgumentType calls olivetin.api.v1.OliveTinApiService.ValidateArgumentType.
|
||||||
|
func (c *oliveTinApiServiceClient) ValidateArgumentType(ctx context.Context, req *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error) {
|
||||||
|
return c.validateArgumentType.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhoAmI calls olivetin.api.v1.OliveTinApiService.WhoAmI.
|
||||||
|
func (c *oliveTinApiServiceClient) WhoAmI(ctx context.Context, req *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error) {
|
||||||
|
return c.whoAmI.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SosReport calls olivetin.api.v1.OliveTinApiService.SosReport.
|
||||||
|
func (c *oliveTinApiServiceClient) SosReport(ctx context.Context, req *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error) {
|
||||||
|
return c.sosReport.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpVars calls olivetin.api.v1.OliveTinApiService.DumpVars.
|
||||||
|
func (c *oliveTinApiServiceClient) DumpVars(ctx context.Context, req *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error) {
|
||||||
|
return c.dumpVars.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpPublicIdActionMap calls olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap.
|
||||||
|
func (c *oliveTinApiServiceClient) DumpPublicIdActionMap(ctx context.Context, req *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error) {
|
||||||
|
return c.dumpPublicIdActionMap.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReadyz calls olivetin.api.v1.OliveTinApiService.GetReadyz.
|
||||||
|
func (c *oliveTinApiServiceClient) GetReadyz(ctx context.Context, req *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error) {
|
||||||
|
return c.getReadyz.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalUserLogin calls olivetin.api.v1.OliveTinApiService.LocalUserLogin.
|
||||||
|
func (c *oliveTinApiServiceClient) LocalUserLogin(ctx context.Context, req *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error) {
|
||||||
|
return c.localUserLogin.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordHash calls olivetin.api.v1.OliveTinApiService.PasswordHash.
|
||||||
|
func (c *oliveTinApiServiceClient) PasswordHash(ctx context.Context, req *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error) {
|
||||||
|
return c.passwordHash.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout calls olivetin.api.v1.OliveTinApiService.Logout.
|
||||||
|
func (c *oliveTinApiServiceClient) Logout(ctx context.Context, req *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) {
|
||||||
|
return c.logout.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventStream calls olivetin.api.v1.OliveTinApiService.EventStream.
|
||||||
|
func (c *oliveTinApiServiceClient) EventStream(ctx context.Context, req *connect.Request[v1.EventStreamRequest]) (*connect.ServerStreamForClient[v1.EventStreamResponse], error) {
|
||||||
|
return c.eventStream.CallServerStream(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDiagnostics calls olivetin.api.v1.OliveTinApiService.GetDiagnostics.
|
||||||
|
func (c *oliveTinApiServiceClient) GetDiagnostics(ctx context.Context, req *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error) {
|
||||||
|
return c.getDiagnostics.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OliveTinApiServiceHandler is an implementation of the olivetin.api.v1.OliveTinApiService service.
|
||||||
|
type OliveTinApiServiceHandler interface {
|
||||||
|
GetDashboardComponents(context.Context, *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error)
|
||||||
|
StartAction(context.Context, *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error)
|
||||||
|
StartActionAndWait(context.Context, *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error)
|
||||||
|
StartActionByGet(context.Context, *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error)
|
||||||
|
StartActionByGetAndWait(context.Context, *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error)
|
||||||
|
KillAction(context.Context, *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error)
|
||||||
|
ExecutionStatus(context.Context, *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error)
|
||||||
|
GetLogs(context.Context, *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error)
|
||||||
|
ValidateArgumentType(context.Context, *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error)
|
||||||
|
WhoAmI(context.Context, *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error)
|
||||||
|
SosReport(context.Context, *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error)
|
||||||
|
DumpVars(context.Context, *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error)
|
||||||
|
DumpPublicIdActionMap(context.Context, *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error)
|
||||||
|
GetReadyz(context.Context, *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error)
|
||||||
|
LocalUserLogin(context.Context, *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error)
|
||||||
|
PasswordHash(context.Context, *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error)
|
||||||
|
Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error)
|
||||||
|
EventStream(context.Context, *connect.Request[v1.EventStreamRequest], *connect.ServerStream[v1.EventStreamResponse]) error
|
||||||
|
GetDiagnostics(context.Context, *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOliveTinApiServiceHandler builds an HTTP handler from the service implementation. It returns
|
||||||
|
// the path on which to mount the handler and the handler itself.
|
||||||
|
//
|
||||||
|
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||||
|
// and JSON codecs. They also support gzip compression.
|
||||||
|
func NewOliveTinApiServiceHandler(svc OliveTinApiServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||||
|
oliveTinApiServiceMethods := v1.File_olivetin_api_v1_olivetin_proto.Services().ByName("OliveTinApiService").Methods()
|
||||||
|
oliveTinApiServiceGetDashboardComponentsHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceGetDashboardComponentsProcedure,
|
||||||
|
svc.GetDashboardComponents,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDashboardComponents")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceStartActionHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceStartActionProcedure,
|
||||||
|
svc.StartAction,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartAction")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceStartActionAndWaitHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceStartActionAndWaitProcedure,
|
||||||
|
svc.StartActionAndWait,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionAndWait")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceStartActionByGetHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceStartActionByGetProcedure,
|
||||||
|
svc.StartActionByGet,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGet")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceStartActionByGetAndWaitHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceStartActionByGetAndWaitProcedure,
|
||||||
|
svc.StartActionByGetAndWait,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("StartActionByGetAndWait")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceKillActionHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceKillActionProcedure,
|
||||||
|
svc.KillAction,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("KillAction")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceExecutionStatusHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceExecutionStatusProcedure,
|
||||||
|
svc.ExecutionStatus,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("ExecutionStatus")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceGetLogsHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceGetLogsProcedure,
|
||||||
|
svc.GetLogs,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetLogs")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceValidateArgumentTypeHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceValidateArgumentTypeProcedure,
|
||||||
|
svc.ValidateArgumentType,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("ValidateArgumentType")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceWhoAmIHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceWhoAmIProcedure,
|
||||||
|
svc.WhoAmI,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("WhoAmI")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceSosReportHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceSosReportProcedure,
|
||||||
|
svc.SosReport,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("SosReport")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceDumpVarsHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceDumpVarsProcedure,
|
||||||
|
svc.DumpVars,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpVars")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceDumpPublicIdActionMapHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceDumpPublicIdActionMapProcedure,
|
||||||
|
svc.DumpPublicIdActionMap,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("DumpPublicIdActionMap")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceGetReadyzHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceGetReadyzProcedure,
|
||||||
|
svc.GetReadyz,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetReadyz")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceLocalUserLoginHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceLocalUserLoginProcedure,
|
||||||
|
svc.LocalUserLogin,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("LocalUserLogin")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServicePasswordHashHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServicePasswordHashProcedure,
|
||||||
|
svc.PasswordHash,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("PasswordHash")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceLogoutHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceLogoutProcedure,
|
||||||
|
svc.Logout,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("Logout")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceEventStreamHandler := connect.NewServerStreamHandler(
|
||||||
|
OliveTinApiServiceEventStreamProcedure,
|
||||||
|
svc.EventStream,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("EventStream")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
oliveTinApiServiceGetDiagnosticsHandler := connect.NewUnaryHandler(
|
||||||
|
OliveTinApiServiceGetDiagnosticsProcedure,
|
||||||
|
svc.GetDiagnostics,
|
||||||
|
connect.WithSchema(oliveTinApiServiceMethods.ByName("GetDiagnostics")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
return "/olivetin.api.v1.OliveTinApiService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case OliveTinApiServiceGetDashboardComponentsProcedure:
|
||||||
|
oliveTinApiServiceGetDashboardComponentsHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceStartActionProcedure:
|
||||||
|
oliveTinApiServiceStartActionHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceStartActionAndWaitProcedure:
|
||||||
|
oliveTinApiServiceStartActionAndWaitHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceStartActionByGetProcedure:
|
||||||
|
oliveTinApiServiceStartActionByGetHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceStartActionByGetAndWaitProcedure:
|
||||||
|
oliveTinApiServiceStartActionByGetAndWaitHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceKillActionProcedure:
|
||||||
|
oliveTinApiServiceKillActionHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceExecutionStatusProcedure:
|
||||||
|
oliveTinApiServiceExecutionStatusHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceGetLogsProcedure:
|
||||||
|
oliveTinApiServiceGetLogsHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceValidateArgumentTypeProcedure:
|
||||||
|
oliveTinApiServiceValidateArgumentTypeHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceWhoAmIProcedure:
|
||||||
|
oliveTinApiServiceWhoAmIHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceSosReportProcedure:
|
||||||
|
oliveTinApiServiceSosReportHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceDumpVarsProcedure:
|
||||||
|
oliveTinApiServiceDumpVarsHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceDumpPublicIdActionMapProcedure:
|
||||||
|
oliveTinApiServiceDumpPublicIdActionMapHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceGetReadyzProcedure:
|
||||||
|
oliveTinApiServiceGetReadyzHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceLocalUserLoginProcedure:
|
||||||
|
oliveTinApiServiceLocalUserLoginHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServicePasswordHashProcedure:
|
||||||
|
oliveTinApiServicePasswordHashHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceLogoutProcedure:
|
||||||
|
oliveTinApiServiceLogoutHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceEventStreamProcedure:
|
||||||
|
oliveTinApiServiceEventStreamHandler.ServeHTTP(w, r)
|
||||||
|
case OliveTinApiServiceGetDiagnosticsProcedure:
|
||||||
|
oliveTinApiServiceGetDiagnosticsHandler.ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedOliveTinApiServiceHandler returns CodeUnimplemented from all methods.
|
||||||
|
type UnimplementedOliveTinApiServiceHandler struct{}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) GetDashboardComponents(context.Context, *connect.Request[v1.GetDashboardComponentsRequest]) (*connect.Response[v1.GetDashboardComponentsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetDashboardComponents is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) StartAction(context.Context, *connect.Request[v1.StartActionRequest]) (*connect.Response[v1.StartActionResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartAction is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) StartActionAndWait(context.Context, *connect.Request[v1.StartActionAndWaitRequest]) (*connect.Response[v1.StartActionAndWaitResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartActionAndWait is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) StartActionByGet(context.Context, *connect.Request[v1.StartActionByGetRequest]) (*connect.Response[v1.StartActionByGetResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartActionByGet is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) StartActionByGetAndWait(context.Context, *connect.Request[v1.StartActionByGetAndWaitRequest]) (*connect.Response[v1.StartActionByGetAndWaitResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) KillAction(context.Context, *connect.Request[v1.KillActionRequest]) (*connect.Response[v1.KillActionResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.KillAction is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) ExecutionStatus(context.Context, *connect.Request[v1.ExecutionStatusRequest]) (*connect.Response[v1.ExecutionStatusResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.ExecutionStatus is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) GetLogs(context.Context, *connect.Request[v1.GetLogsRequest]) (*connect.Response[v1.GetLogsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetLogs is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) ValidateArgumentType(context.Context, *connect.Request[v1.ValidateArgumentTypeRequest]) (*connect.Response[v1.ValidateArgumentTypeResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.ValidateArgumentType is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) WhoAmI(context.Context, *connect.Request[v1.WhoAmIRequest]) (*connect.Response[v1.WhoAmIResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.WhoAmI is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) SosReport(context.Context, *connect.Request[v1.SosReportRequest]) (*connect.Response[v1.SosReportResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.SosReport is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) DumpVars(context.Context, *connect.Request[v1.DumpVarsRequest]) (*connect.Response[v1.DumpVarsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.DumpVars is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) DumpPublicIdActionMap(context.Context, *connect.Request[v1.DumpPublicIdActionMapRequest]) (*connect.Response[v1.DumpPublicIdActionMapResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) GetReadyz(context.Context, *connect.Request[v1.GetReadyzRequest]) (*connect.Response[v1.GetReadyzResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetReadyz is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) LocalUserLogin(context.Context, *connect.Request[v1.LocalUserLoginRequest]) (*connect.Response[v1.LocalUserLoginResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.LocalUserLogin is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) PasswordHash(context.Context, *connect.Request[v1.PasswordHashRequest]) (*connect.Response[v1.PasswordHashResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.PasswordHash is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) Logout(context.Context, *connect.Request[v1.LogoutRequest]) (*connect.Response[v1.LogoutResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.Logout is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) EventStream(context.Context, *connect.Request[v1.EventStreamRequest], *connect.ServerStream[v1.EventStreamResponse]) error {
|
||||||
|
return connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.EventStream is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedOliveTinApiServiceHandler) GetDiagnostics(context.Context, *connect.Request[v1.GetDiagnosticsRequest]) (*connect.Response[v1.GetDiagnosticsResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("olivetin.api.v1.OliveTinApiService.GetDiagnostics is not implemented"))
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,7 @@ require (
|
||||||
github.com/google/go-containerregistry v0.20.6 // indirect
|
github.com/google/go-containerregistry v0.20.6 // indirect
|
||||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
|
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4 // indirect
|
||||||
github.com/jdx/go-netrc v1.0.0 // indirect
|
github.com/jdx/go-netrc v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4 h1:MIZEqAaeMP1/saH0w6I5mzGKSv2lw8fAO7Hm2FgJb9k=
|
||||||
|
github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||||
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
|
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
|
||||||
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
|
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
|
||||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
|
github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,595 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
ctx "context"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
|
||||||
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
|
apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||||
|
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||||
|
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oliveTinAPI struct {
|
||||||
|
executor *executor.Executor
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
connectedClients []*connectedClients
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectedClients struct {
|
||||||
|
channel chan *apiv1.EventStreamResponse
|
||||||
|
AuthenticatedUser *acl.AuthenticatedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
|
||||||
|
ret := &apiv1.KillActionResponse{
|
||||||
|
ExecutionTrackingId: req.Msg.ExecutionTrackingId,
|
||||||
|
}
|
||||||
|
|
||||||
|
var execReqLogEntry *executor.InternalLogEntry
|
||||||
|
|
||||||
|
execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId)
|
||||||
|
|
||||||
|
if !ret.Found {
|
||||||
|
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
|
||||||
|
|
||||||
|
action := api.cfg.FindAction(execReqLogEntry.ActionTitle)
|
||||||
|
|
||||||
|
if action == nil {
|
||||||
|
log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
|
||||||
|
ret.Killed = false
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
api.killActionByTrackingId(user, action, execReqLogEntry, ret)
|
||||||
|
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
|
||||||
|
if !acl.IsAllowedKill(api.cfg, user, action) {
|
||||||
|
log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
|
||||||
|
ret.Killed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
err := api.executor.Kill(execReqLogEntry)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Killing execution request err: %v", err)
|
||||||
|
ret.AlreadyCompleted = true
|
||||||
|
ret.Killed = false
|
||||||
|
} else {
|
||||||
|
ret.Killed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.StartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
|
||||||
|
args := make(map[string]string)
|
||||||
|
|
||||||
|
for _, arg := range req.Msg.Arguments {
|
||||||
|
args[arg.Name] = arg.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
api.executor.MapActionIdToBindingLock.RLock()
|
||||||
|
pair := api.executor.MapActionIdToBinding[req.Msg.ActionId]
|
||||||
|
api.executor.MapActionIdToBindingLock.RUnlock()
|
||||||
|
|
||||||
|
if pair == nil || pair.Action == nil {
|
||||||
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticatedUser := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
execReq := executor.ExecutionRequest{
|
||||||
|
Action: pair.Action,
|
||||||
|
EntityPrefix: pair.EntityPrefix,
|
||||||
|
TrackingID: req.Msg.UniqueTrackingId,
|
||||||
|
Arguments: args,
|
||||||
|
AuthenticatedUser: authenticatedUser,
|
||||||
|
Cfg: api.cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
api.executor.ExecRequest(&execReq)
|
||||||
|
|
||||||
|
ret := &apiv1.StartActionResponse{
|
||||||
|
ExecutionTrackingId: execReq.TrackingID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1.PasswordHashRequest]) (*connect.Response[apiv1.PasswordHashResponse], error) {
|
||||||
|
hash, err := createHash(req.Msg.Password)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := &apiv1.PasswordHashResponse{
|
||||||
|
Hash: hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
|
||||||
|
match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
|
||||||
|
|
||||||
|
if match {
|
||||||
|
//grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"username": req.Msg.Username,
|
||||||
|
}).Info("LocalUserLogin: User logged in successfully.")
|
||||||
|
} else {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"username": req.Msg.Username,
|
||||||
|
}).Warn("LocalUserLogin: User login failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(&apiv1.LocalUserLoginResponse{
|
||||||
|
Success: match,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
|
||||||
|
args := make(map[string]string)
|
||||||
|
|
||||||
|
for _, arg := range req.Msg.Arguments {
|
||||||
|
args[arg.Name] = arg.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
execReq := executor.ExecutionRequest{
|
||||||
|
Action: api.executor.FindActionBindingByID(req.Msg.ActionId),
|
||||||
|
TrackingID: uuid.NewString(),
|
||||||
|
Arguments: args,
|
||||||
|
AuthenticatedUser: user,
|
||||||
|
Cfg: api.cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg, _ := api.executor.ExecRequest(&execReq)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
|
||||||
|
LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
|
||||||
|
}), nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("execution not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) {
|
||||||
|
args := make(map[string]string)
|
||||||
|
|
||||||
|
execReq := executor.ExecutionRequest{
|
||||||
|
Action: api.executor.FindActionBindingByID(req.Msg.ActionId),
|
||||||
|
TrackingID: uuid.NewString(),
|
||||||
|
Arguments: args,
|
||||||
|
AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
|
||||||
|
Cfg: api.cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
|
||||||
|
|
||||||
|
return connect.NewResponse(&apiv1.StartActionByGetResponse{
|
||||||
|
ExecutionTrackingId: uniqueTrackingId,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
|
||||||
|
args := make(map[string]string)
|
||||||
|
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
execReq := executor.ExecutionRequest{
|
||||||
|
Action: api.executor.FindActionBindingByID(req.Msg.ActionId),
|
||||||
|
TrackingID: uuid.NewString(),
|
||||||
|
Arguments: args,
|
||||||
|
AuthenticatedUser: user,
|
||||||
|
Cfg: api.cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg, _ := api.executor.ExecRequest(&execReq)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
|
||||||
|
LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
|
||||||
|
}), nil
|
||||||
|
} else {
|
||||||
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
|
||||||
|
pble := &apiv1.LogEntry{
|
||||||
|
ActionTitle: logEntry.ActionTitle,
|
||||||
|
ActionIcon: logEntry.ActionIcon,
|
||||||
|
ActionId: logEntry.ActionId,
|
||||||
|
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
||||||
|
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
||||||
|
DatetimeIndex: logEntry.Index,
|
||||||
|
Output: logEntry.Output,
|
||||||
|
TimedOut: logEntry.TimedOut,
|
||||||
|
Blocked: logEntry.Blocked,
|
||||||
|
ExitCode: logEntry.ExitCode,
|
||||||
|
Tags: logEntry.Tags,
|
||||||
|
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
||||||
|
ExecutionStarted: logEntry.ExecutionStarted,
|
||||||
|
ExecutionFinished: logEntry.ExecutionFinished,
|
||||||
|
User: logEntry.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pble.ExecutionFinished {
|
||||||
|
pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, api.cfg.FindAction(logEntry.ActionConfigTitle))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pble
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
|
||||||
|
logEntry, ok := api.executor.GetLog(executionTrackingId)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return logEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
|
||||||
|
var ile *executor.InternalLogEntry
|
||||||
|
|
||||||
|
logs := api.executor.GetLogsByActionId(actionId)
|
||||||
|
|
||||||
|
if len(logs) == 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// Get last log entry
|
||||||
|
ile = logs[len(logs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
|
||||||
|
res := &apiv1.ExecutionStatusResponse{}
|
||||||
|
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
var ile *executor.InternalLogEntry
|
||||||
|
|
||||||
|
if req.Msg.ExecutionTrackingId != "" {
|
||||||
|
ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ile == nil {
|
||||||
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId))
|
||||||
|
} else {
|
||||||
|
res.LogEntry = api.internalLogEntryToPb(ile, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
|
||||||
|
log.Infof("Watch")
|
||||||
|
|
||||||
|
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
|
||||||
|
log.Errorf("Execution not found: %v", req.ExecutionUuid)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
if logEntry.ExecutionStarted {
|
||||||
|
for !logEntry.ExecutionCompleted {
|
||||||
|
tmp := make([]byte, 256)
|
||||||
|
|
||||||
|
red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
|
||||||
|
|
||||||
|
log.Infof("%v %v", red, err)
|
||||||
|
|
||||||
|
srv.Send(&apiv1.WatchExecutionUpdate{
|
||||||
|
Update: string(tmp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
|
||||||
|
//user := acl.UserFromContext(ctx, cfg)
|
||||||
|
|
||||||
|
//grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
|
||||||
|
//grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardComponentsRequest]) (*connect.Response[apiv1.GetDashboardComponentsResponse], error) {
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
|
||||||
|
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
|
||||||
|
}
|
||||||
|
|
||||||
|
res := buildDashboardResponse(api.executor, api.cfg, user)
|
||||||
|
|
||||||
|
/*
|
||||||
|
if len(res.Actions) == 0 {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"username": user.Username,
|
||||||
|
"usergroupLine": user.UsergroupLine,
|
||||||
|
"provider": user.Provider,
|
||||||
|
"acls": user.Acls,
|
||||||
|
"availableActions": len(api.cfg.Actions),
|
||||||
|
}).Warn("Zero actions found for user")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
log.Tracef("GetDashboardComponents: %v", res)
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
ret := &apiv1.GetLogsResponse{}
|
||||||
|
|
||||||
|
logEntries, countRemaining := api.executor.GetLogTrackingIds(req.Msg.StartOffset, api.cfg.LogHistoryPageSize)
|
||||||
|
|
||||||
|
for _, logEntry := range logEntries {
|
||||||
|
action := api.cfg.FindAction(logEntry.ActionTitle)
|
||||||
|
|
||||||
|
if action == nil || acl.IsAllowedLogs(api.cfg, user, action) {
|
||||||
|
pbLogEntry := api.internalLogEntryToPb(logEntry, user)
|
||||||
|
|
||||||
|
ret.Logs = append(ret.Logs, pbLogEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.CountRemaining = countRemaining
|
||||||
|
ret.PageSize = api.cfg.LogHistoryPageSize
|
||||||
|
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This function is ONLY a helper for the UI - the arguments are validated properly
|
||||||
|
on the StartAction -> Executor chain. This is here basically to provide helpful
|
||||||
|
error messages more quickly before starting the action.
|
||||||
|
*/
|
||||||
|
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
|
||||||
|
err := executor.TypeSafetyCheck("", req.Msg.Value, req.Msg.Type)
|
||||||
|
desc := ""
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
desc = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
|
||||||
|
Valid: err == nil,
|
||||||
|
Description: desc,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
|
||||||
|
user := acl.UserFromContext(ctx, api.cfg)
|
||||||
|
|
||||||
|
res := &apiv1.WhoAmIResponse{
|
||||||
|
AuthenticatedUser: user.Username,
|
||||||
|
Usergroup: user.UsergroupLine,
|
||||||
|
Provider: user.Provider,
|
||||||
|
Sid: user.SID,
|
||||||
|
Acls: user.Acls,
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
|
||||||
|
sos := installationinfo.GetSosReport()
|
||||||
|
|
||||||
|
if !api.cfg.InsecureAllowDumpSos {
|
||||||
|
log.Info(sos)
|
||||||
|
sos = "Your SOS Report has been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpSos: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := &apiv1.SosReportResponse{
|
||||||
|
Alert: sos,
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.DumpVarsRequest]) (*connect.Response[apiv1.DumpVarsResponse], error) {
|
||||||
|
res := &apiv1.DumpVarsResponse{}
|
||||||
|
|
||||||
|
if !api.cfg.InsecureAllowDumpVars {
|
||||||
|
res.Alert = "Dumping variables is not allowed by default because it is insecure."
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
|
||||||
|
res.Contents = sv.GetAll()
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
|
||||||
|
res := &apiv1.DumpPublicIdActionMapResponse{}
|
||||||
|
res.Contents = make(map[string]*apiv1.ActionEntityPair)
|
||||||
|
|
||||||
|
if !api.cfg.InsecureAllowDumpActionMap {
|
||||||
|
res.Alert = "Dumping Public IDs is disallowed."
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
api.executor.MapActionIdToBindingLock.RLock()
|
||||||
|
|
||||||
|
for k, v := range api.executor.MapActionIdToBinding {
|
||||||
|
res.Contents[k] = &apiv1.ActionEntityPair{
|
||||||
|
ActionTitle: v.Action.Title,
|
||||||
|
EntityPrefix: v.EntityPrefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.executor.MapActionIdToBindingLock.RUnlock()
|
||||||
|
|
||||||
|
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.GetReadyzRequest]) (*connect.Response[apiv1.GetReadyzResponse], error) {
|
||||||
|
res := &apiv1.GetReadyzResponse{
|
||||||
|
Status: "OK",
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
|
||||||
|
log.Debugf("EventStream: %v", req.Msg)
|
||||||
|
|
||||||
|
client := &connectedClients{
|
||||||
|
channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
|
||||||
|
AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
|
||||||
|
|
||||||
|
api.connectedClients = append(api.connectedClients, client)
|
||||||
|
|
||||||
|
// loop over client channel and send events to connectedClient
|
||||||
|
for msg := range client.channel {
|
||||||
|
log.Debugf("Sending event to client: %v", msg)
|
||||||
|
if err := srv.Send(msg); err != nil {
|
||||||
|
log.Errorf("Error sending event to client: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("EventStream: client disconnected")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) OnActionMapRebuilt() {
|
||||||
|
for _, client := range api.connectedClients {
|
||||||
|
select {
|
||||||
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
|
Event: &apiv1.EventStreamResponse_ConfigChanged{
|
||||||
|
ConfigChanged: &apiv1.EventConfigChanged{},
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
log.Warnf("EventStream: client channel is full, dropping message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
|
||||||
|
for _, client := range api.connectedClients {
|
||||||
|
select {
|
||||||
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
|
Event: &apiv1.EventStreamResponse_ExecutionStarted{
|
||||||
|
ExecutionStarted: &apiv1.EventExecutionStarted{
|
||||||
|
LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
log.Warnf("EventStream: client channel is full, dropping message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
|
||||||
|
for _, client := range api.connectedClients {
|
||||||
|
select {
|
||||||
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
|
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
||||||
|
ExecutionFinished: &apiv1.EventExecutionFinished{
|
||||||
|
LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
log.Warnf("EventStream: client channel is full, dropping message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
|
||||||
|
res := &apiv1.GetDiagnosticsResponse{
|
||||||
|
SshFoundKey: installationinfo.Runtime.SshFoundKey,
|
||||||
|
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
return connect.NewResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
|
||||||
|
for _, client := range api.connectedClients {
|
||||||
|
select {
|
||||||
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
|
Event: &apiv1.EventStreamResponse_OutputChunk{
|
||||||
|
OutputChunk: &apiv1.EventOutputChunk{
|
||||||
|
Output: string(content),
|
||||||
|
ExecutionTrackingId: executionTrackingId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
log.Warnf("EventStream: client channel is full, dropping message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(ex *executor.Executor) *oliveTinAPI {
|
||||||
|
server := oliveTinAPI{}
|
||||||
|
server.cfg = ex.Cfg
|
||||||
|
server.executor = ex
|
||||||
|
|
||||||
|
ex.AddListener(&server)
|
||||||
|
return &server
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewHandler(ex *executor.Executor) (string, http.Handler) {
|
||||||
|
server := newServer(ex)
|
||||||
|
|
||||||
|
return apiv1connect.NewOliveTinApiServiceHandler(server)
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,32 @@
|
||||||
package grpcapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
|
||||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||||
"sort"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardRenderRequest struct {
|
type DashboardRenderRequest struct {
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *acl.AuthenticatedUser
|
||||||
AllowedActionTitles []string `json:"allows_action_titles"`
|
AllowedActionTitles []string `json:"allows_action_titles"`
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
ex *executor.Executor
|
||||||
|
|
||||||
|
usedActions map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
|
||||||
|
for _, action := range rr.cfg.Actions {
|
||||||
|
log.Infof("Checking action %s against %s", title, action.Title)
|
||||||
|
if action.Title == title {
|
||||||
|
return buildAction(action.ID, nil, rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse {
|
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse {
|
||||||
|
|
@ -22,18 +35,7 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
|
||||||
AuthenticatedUserProvider: user.Provider,
|
AuthenticatedUserProvider: user.Provider,
|
||||||
}
|
}
|
||||||
|
|
||||||
ex.MapActionIdToBindingLock.RLock()
|
/*
|
||||||
|
|
||||||
for actionId, actionBinding := range ex.MapActionIdToBinding {
|
|
||||||
if !acl.IsAllowedView(cfg, user, actionBinding.Action) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Actions = append(res.Actions, buildAction(actionId, actionBinding, user))
|
|
||||||
}
|
|
||||||
|
|
||||||
ex.MapActionIdToBindingLock.RUnlock()
|
|
||||||
|
|
||||||
sort.Slice(res.Actions, func(i, j int) bool {
|
sort.Slice(res.Actions, func(i, j int) bool {
|
||||||
if res.Actions[i].Order == res.Actions[j].Order {
|
if res.Actions[i].Order == res.Actions[j].Order {
|
||||||
return res.Actions[i].Title < res.Actions[j].Title
|
return res.Actions[i].Title < res.Actions[j].Title
|
||||||
|
|
@ -41,15 +43,17 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
|
||||||
return res.Actions[i].Order < res.Actions[j].Order
|
return res.Actions[i].Order < res.Actions[j].Order
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
rr := &DashboardRenderRequest{
|
rr := &DashboardRenderRequest{
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
AllowedActionTitles: getActionTitles(res.Actions),
|
// AllowedActionTitles: getActionTitles(res.Actions),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
ex: ex,
|
||||||
|
usedActions: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
res.EffectivePolicy = buildEffectivePolicy(user.EffectivePolicy)
|
res.EffectivePolicy = buildEffectivePolicy(user.EffectivePolicy)
|
||||||
res.Diagnostics = buildDiagnostics(res.EffectivePolicy.ShowDiagnostics)
|
|
||||||
res.Dashboards = dashboardCfgToPb(rr)
|
res.Dashboards = dashboardCfgToPb(rr)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
@ -74,25 +78,14 @@ func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePo
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDiagnostics(showDiagnostics bool) *apiv1.Diagnostics {
|
func buildAction(actionId string, actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action {
|
||||||
ret := &apiv1.Diagnostics{}
|
|
||||||
|
|
||||||
if showDiagnostics {
|
|
||||||
ret.SshFoundKey = installationinfo.Runtime.SshFoundKey
|
|
||||||
ret.SshFoundConfig = installationinfo.Runtime.SshFoundConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *apiv1.Action {
|
|
||||||
action := actionBinding.Action
|
action := actionBinding.Action
|
||||||
|
|
||||||
btn := apiv1.Action{
|
btn := apiv1.Action{
|
||||||
Id: actionId,
|
Id: actionId,
|
||||||
Title: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Title),
|
Title: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Title),
|
||||||
Icon: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Icon),
|
Icon: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Icon),
|
||||||
CanExec: acl.IsAllowedExec(cfg, user, action),
|
CanExec: acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action),
|
||||||
PopupOnStart: action.PopupOnStart,
|
PopupOnStart: action.PopupOnStart,
|
||||||
Order: int32(actionBinding.ConfigOrder),
|
Order: int32(actionBinding.ConfigOrder),
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package grpcapi
|
package api
|
||||||
|
|
||||||
// Thank you: https://stackoverflow.com/questions/42102496/testing-a-grpc-service
|
// Thank you: https://stackoverflow.com/questions/42102496/testing-a-grpc-service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/test/bufconn"
|
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package grpcapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
package grpcapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
|
func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.Dashboard {
|
||||||
ret := make([]*apiv1.DashboardComponent, 0)
|
ret := make([]*apiv1.Dashboard, 0)
|
||||||
|
|
||||||
for _, dashboard := range cfg.Dashboards {
|
for _, dashboard := range rr.cfg.Dashboards {
|
||||||
pbdb := &apiv1.DashboardComponent{
|
pbdb := &apiv1.Dashboard{
|
||||||
Type: "dashboard",
|
|
||||||
Title: dashboard.Title,
|
Title: dashboard.Title,
|
||||||
Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
|
Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
|
||||||
|
// Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
if len(pbdb.Contents) == 0 {
|
if len(pbdb.Contents) == 0 {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"dashboard": dashboard.Title,
|
"dashboard": dashboard.Title,
|
||||||
|
|
@ -24,13 +24,52 @@ func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
|
||||||
}).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui")
|
}).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
ret = append(ret, pbdb)
|
ret = append(ret, pbdb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ret = append(ret, buildDefaultDashboard(rr))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
|
||||||
|
fieldset := &apiv1.DashboardComponent{
|
||||||
|
Type: "fieldset",
|
||||||
|
Contents: make([]*apiv1.DashboardComponent, 0),
|
||||||
|
Title: "Default",
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := make([]*apiv1.Action, 0)
|
||||||
|
|
||||||
|
for id, binding := range rr.ex.MapActionIdToBinding {
|
||||||
|
if binding.Action.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr.usedActions[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rr.usedActions[id] = true
|
||||||
|
actions = append(actions, buildAction(id, binding, rr))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
fieldset.Contents = append(fieldset.Contents, &apiv1.DashboardComponent{
|
||||||
|
Type: "link",
|
||||||
|
Title: action.Title,
|
||||||
|
Icon: action.Icon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiv1.Dashboard{
|
||||||
|
Title: "Default",
|
||||||
|
Contents: []*apiv1.DashboardComponent{fieldset},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
|
func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
|
||||||
ret := make([]*apiv1.DashboardComponent, 0)
|
ret := make([]*apiv1.DashboardComponent, 0)
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package grpcapi
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
|
@ -21,6 +21,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultExitCodeNotExecuted = -1337
|
||||||
|
MaxTriggerDepth = 10
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
|
metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "olivetin_actions_requested_count",
|
Name: "olivetin_actions_requested_count",
|
||||||
|
|
@ -64,6 +69,7 @@ type ExecutionRequest struct {
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *acl.AuthenticatedUser
|
||||||
EntityPrefix string
|
EntityPrefix string
|
||||||
|
TriggerDepth int
|
||||||
|
|
||||||
logEntry *InternalLogEntry
|
logEntry *InternalLogEntry
|
||||||
finalParsedCommand string
|
finalParsedCommand string
|
||||||
|
|
@ -88,6 +94,7 @@ type InternalLogEntry struct {
|
||||||
Username string
|
Username string
|
||||||
Index int64
|
Index int64
|
||||||
EntityPrefix string
|
EntityPrefix string
|
||||||
|
ActionConfigTitle string // This is the title of the action as defined in the config, not the final parsed title.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The following 3 properties are obviously on Action normally, but it's useful
|
The following 3 properties are obviously on Action normally, but it's useful
|
||||||
|
|
@ -243,7 +250,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
|
||||||
DatetimeStarted: time.Now(),
|
DatetimeStarted: time.Now(),
|
||||||
ExecutionTrackingID: req.TrackingID,
|
ExecutionTrackingID: req.TrackingID,
|
||||||
Output: "",
|
Output: "",
|
||||||
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
ExitCode: DefaultExitCodeNotExecuted,
|
||||||
ExecutionStarted: false,
|
ExecutionStarted: false,
|
||||||
ExecutionFinished: false,
|
ExecutionFinished: false,
|
||||||
ActionId: "",
|
ActionId: "",
|
||||||
|
|
@ -440,6 +447,7 @@ func stepRequestAction(req *ExecutionRequest) bool {
|
||||||
|
|
||||||
metricActionsRequested.Inc()
|
metricActionsRequested.Inc()
|
||||||
|
|
||||||
|
req.logEntry.ActionConfigTitle = req.Action.Title
|
||||||
req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
|
req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
|
||||||
req.logEntry.ActionIcon = req.Action.Icon
|
req.logEntry.ActionIcon = req.Action.Icon
|
||||||
req.logEntry.ActionId = req.Action.ID
|
req.logEntry.ActionId = req.Action.ID
|
||||||
|
|
@ -651,6 +659,15 @@ func stepTrigger(req *ExecutionRequest) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.TriggerDepth >= MaxTriggerDepth {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"actionTitle": req.logEntry.ActionTitle,
|
||||||
|
"depth": req.TriggerDepth,
|
||||||
|
}).Warnf("Trigger action reached maximum depth of %v. Not triggering further actions.", MaxTriggerDepth)
|
||||||
|
req.logEntry.Output += fmt.Sprintf("OliveTin::trigger - this action reached maximum trigger depth of %v. Not triggering further actions.", MaxTriggerDepth)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
|
if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
|
||||||
log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
|
log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
|
||||||
}
|
}
|
||||||
|
|
@ -669,6 +686,7 @@ func triggerLoop(req *ExecutionRequest) {
|
||||||
AuthenticatedUser: req.AuthenticatedUser,
|
AuthenticatedUser: req.AuthenticatedUser,
|
||||||
Arguments: req.Arguments,
|
Arguments: req.Arguments,
|
||||||
Cfg: req.Cfg,
|
Cfg: req.Cfg,
|
||||||
|
TriggerDepth: req.TriggerDepth + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
req.executor.ExecRequest(trigger)
|
req.executor.ExecRequest(trigger)
|
||||||
|
|
|
||||||
|
|
@ -1,512 +0,0 @@
|
||||||
package grpcapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
ctx "context"
|
|
||||||
|
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
|
||||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
|
||||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfg *config.Config
|
|
||||||
)
|
|
||||||
|
|
||||||
type oliveTinAPI struct {
|
|
||||||
// Uncomment this if you want to allow undefined methods during dev.
|
|
||||||
// apiv1.UnimplementedOliveTinApiServiceServer
|
|
||||||
|
|
||||||
executor *executor.Executor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest) (*apiv1.KillActionResponse, error) {
|
|
||||||
ret := &apiv1.KillActionResponse{
|
|
||||||
ExecutionTrackingId: req.ExecutionTrackingId,
|
|
||||||
}
|
|
||||||
|
|
||||||
var execReqLogEntry *executor.InternalLogEntry
|
|
||||||
|
|
||||||
execReqLogEntry, ret.Found = api.executor.GetLog(req.ExecutionTrackingId)
|
|
||||||
|
|
||||||
if !ret.Found {
|
|
||||||
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId)
|
|
||||||
|
|
||||||
action := cfg.FindAction(execReqLogEntry.ActionTitle)
|
|
||||||
|
|
||||||
if action == nil {
|
|
||||||
log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
|
|
||||||
ret.Killed = false
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
api.killActionByTrackingId(user, action, execReqLogEntry, ret)
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
|
|
||||||
if !acl.IsAllowedKill(cfg, user, action) {
|
|
||||||
log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
|
|
||||||
ret.Killed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
err := api.executor.Kill(execReqLogEntry)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Killing execution request err: %v", err)
|
|
||||||
ret.AlreadyCompleted = true
|
|
||||||
ret.Killed = false
|
|
||||||
} else {
|
|
||||||
ret.Killed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *apiv1.StartActionRequest) (*apiv1.StartActionResponse, error) {
|
|
||||||
args := make(map[string]string)
|
|
||||||
|
|
||||||
for _, arg := range req.Arguments {
|
|
||||||
args[arg.Name] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
api.executor.MapActionIdToBindingLock.RLock()
|
|
||||||
pair := api.executor.MapActionIdToBinding[req.ActionId]
|
|
||||||
api.executor.MapActionIdToBindingLock.RUnlock()
|
|
||||||
|
|
||||||
if pair == nil || pair.Action == nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "Action not found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticatedUser := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
|
||||||
Action: pair.Action,
|
|
||||||
EntityPrefix: pair.EntityPrefix,
|
|
||||||
TrackingID: req.UniqueTrackingId,
|
|
||||||
Arguments: args,
|
|
||||||
AuthenticatedUser: authenticatedUser,
|
|
||||||
Cfg: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
api.executor.ExecRequest(&execReq)
|
|
||||||
|
|
||||||
return &apiv1.StartActionResponse{
|
|
||||||
ExecutionTrackingId: execReq.TrackingID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *apiv1.PasswordHashRequest) (*httpbody.HttpBody, error) {
|
|
||||||
hash, err := createHash(req.Password)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "Error creating hash.")
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := &httpbody.HttpBody{
|
|
||||||
ContentType: "text/plain",
|
|
||||||
Data: []byte("Your password hash is: " + hash),
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *apiv1.LocalUserLoginRequest) (*apiv1.LocalUserLoginResponse, error) {
|
|
||||||
match := checkUserPassword(cfg, req.Username, req.Password)
|
|
||||||
|
|
||||||
if match {
|
|
||||||
grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"username": req.Username,
|
|
||||||
}).Info("LocalUserLogin: User logged in successfully.")
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"username": req.Username,
|
|
||||||
}).Warn("LocalUserLogin: User login failed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apiv1.LocalUserLoginResponse{
|
|
||||||
Success: match,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActionAndWaitRequest) (*apiv1.StartActionAndWaitResponse, error) {
|
|
||||||
args := make(map[string]string)
|
|
||||||
|
|
||||||
for _, arg := range req.Arguments {
|
|
||||||
args[arg.Name] = arg.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
|
||||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
|
||||||
TrackingID: uuid.NewString(),
|
|
||||||
Arguments: args,
|
|
||||||
AuthenticatedUser: user,
|
|
||||||
Cfg: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
wg, _ := api.executor.ExecRequest(&execReq)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return &apiv1.StartActionAndWaitResponse{
|
|
||||||
LogEntry: internalLogEntryToPb(internalLogEntry, user),
|
|
||||||
}, nil
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("execution not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *apiv1.StartActionByGetRequest) (*apiv1.StartActionByGetResponse, error) {
|
|
||||||
args := make(map[string]string)
|
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
|
||||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
|
||||||
TrackingID: uuid.NewString(),
|
|
||||||
Arguments: args,
|
|
||||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
|
||||||
Cfg: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
|
|
||||||
|
|
||||||
return &apiv1.StartActionByGetResponse{
|
|
||||||
ExecutionTrackingId: uniqueTrackingId,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.StartActionByGetAndWaitRequest) (*apiv1.StartActionByGetAndWaitResponse, error) {
|
|
||||||
args := make(map[string]string)
|
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
|
||||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
|
||||||
TrackingID: uuid.NewString(),
|
|
||||||
Arguments: args,
|
|
||||||
AuthenticatedUser: user,
|
|
||||||
Cfg: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
wg, _ := api.executor.ExecRequest(&execReq)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return &apiv1.StartActionByGetAndWaitResponse{
|
|
||||||
LogEntry: internalLogEntryToPb(internalLogEntry, user),
|
|
||||||
}, nil
|
|
||||||
} else {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "Execution not found.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
|
|
||||||
pble := &apiv1.LogEntry{
|
|
||||||
ActionTitle: logEntry.ActionTitle,
|
|
||||||
ActionIcon: logEntry.ActionIcon,
|
|
||||||
ActionId: logEntry.ActionId,
|
|
||||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
|
||||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
|
||||||
DatetimeIndex: logEntry.Index,
|
|
||||||
Output: logEntry.Output,
|
|
||||||
TimedOut: logEntry.TimedOut,
|
|
||||||
Blocked: logEntry.Blocked,
|
|
||||||
ExitCode: logEntry.ExitCode,
|
|
||||||
Tags: logEntry.Tags,
|
|
||||||
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
|
||||||
ExecutionStarted: logEntry.ExecutionStarted,
|
|
||||||
ExecutionFinished: logEntry.ExecutionFinished,
|
|
||||||
User: logEntry.Username,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pble.ExecutionFinished {
|
|
||||||
pble.CanKill = acl.IsAllowedKill(cfg, authenticatedUser, cfg.FindAction(logEntry.ActionTitle))
|
|
||||||
}
|
|
||||||
|
|
||||||
return pble
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
|
|
||||||
logEntry, ok := api.executor.GetLog(executionTrackingId)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return logEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
|
|
||||||
var ile *executor.InternalLogEntry
|
|
||||||
|
|
||||||
logs := api.executor.GetLogsByActionId(actionId)
|
|
||||||
|
|
||||||
if len(logs) == 0 {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
// Get last log entry
|
|
||||||
ile = logs[len(logs)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionStatusRequest) (*apiv1.ExecutionStatusResponse, error) {
|
|
||||||
res := &apiv1.ExecutionStatusResponse{}
|
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
var ile *executor.InternalLogEntry
|
|
||||||
|
|
||||||
if req.ExecutionTrackingId != "" {
|
|
||||||
ile = getExecutionStatusByTrackingID(api, req.ExecutionTrackingId)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
ile = getMostRecentExecutionStatusById(api, req.ActionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ile == nil {
|
|
||||||
return nil, status.Error(codes.NotFound, "Execution not found")
|
|
||||||
} else {
|
|
||||||
res.LogEntry = internalLogEntryToPb(ile, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
|
|
||||||
log.Infof("Watch")
|
|
||||||
|
|
||||||
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
|
|
||||||
log.Errorf("Execution not found: %v", req.ExecutionUuid)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
if logEntry.ExecutionStarted {
|
|
||||||
for !logEntry.ExecutionCompleted {
|
|
||||||
tmp := make([]byte, 256)
|
|
||||||
|
|
||||||
red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
|
|
||||||
|
|
||||||
log.Infof("%v %v", red, err)
|
|
||||||
|
|
||||||
srv.Send(&apiv1.WatchExecutionUpdate{
|
|
||||||
Update: string(tmp),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *apiv1.LogoutRequest) (*httpbody.HttpBody, error) {
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
|
|
||||||
grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *apiv1.GetDashboardComponentsRequest) (*apiv1.GetDashboardComponentsResponse, error) {
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
if user.IsGuest() && cfg.AuthRequireGuestsToLogin {
|
|
||||||
return nil, status.Errorf(codes.PermissionDenied, "Guests are not allowed to access the dashboard.")
|
|
||||||
}
|
|
||||||
|
|
||||||
res := buildDashboardResponse(api.executor, cfg, user)
|
|
||||||
|
|
||||||
if len(res.Actions) == 0 {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"username": user.Username,
|
|
||||||
"usergroupLine": user.UsergroupLine,
|
|
||||||
"provider": user.Provider,
|
|
||||||
"acls": user.Acls,
|
|
||||||
"availableActions": len(cfg.Actions),
|
|
||||||
}).Warn("Zero actions found for user")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("GetDashboardComponents: %v", res)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *apiv1.GetLogsRequest) (*apiv1.GetLogsResponse, error) {
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
ret := &apiv1.GetLogsResponse{}
|
|
||||||
|
|
||||||
logEntries, countRemaining := api.executor.GetLogTrackingIds(req.StartOffset, cfg.LogHistoryPageSize)
|
|
||||||
|
|
||||||
for _, logEntry := range logEntries {
|
|
||||||
action := cfg.FindAction(logEntry.ActionTitle)
|
|
||||||
|
|
||||||
if action == nil || acl.IsAllowedLogs(cfg, user, action) {
|
|
||||||
pbLogEntry := internalLogEntryToPb(logEntry, user)
|
|
||||||
|
|
||||||
ret.Logs = append(ret.Logs, pbLogEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.CountRemaining = countRemaining
|
|
||||||
ret.PageSize = cfg.LogHistoryPageSize
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This function is ONLY a helper for the UI - the arguments are validated properly
|
|
||||||
on the StartAction -> Executor chain. This is here basically to provide helpful
|
|
||||||
error messages more quickly before starting the action.
|
|
||||||
*/
|
|
||||||
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *apiv1.ValidateArgumentTypeRequest) (*apiv1.ValidateArgumentTypeResponse, error) {
|
|
||||||
err := executor.TypeSafetyCheck("", req.Value, req.Type)
|
|
||||||
desc := ""
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
desc = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apiv1.ValidateArgumentTypeResponse{
|
|
||||||
Valid: err == nil,
|
|
||||||
Description: desc,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *apiv1.WhoAmIRequest) (*apiv1.WhoAmIResponse, error) {
|
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
|
||||||
|
|
||||||
res := &apiv1.WhoAmIResponse{
|
|
||||||
AuthenticatedUser: user.Username,
|
|
||||||
Usergroup: user.UsergroupLine,
|
|
||||||
Provider: user.Provider,
|
|
||||||
Sid: user.SID,
|
|
||||||
Acls: user.Acls,
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *apiv1.SosReportRequest) (*httpbody.HttpBody, error) {
|
|
||||||
sos := installationinfo.GetSosReport()
|
|
||||||
|
|
||||||
if !cfg.InsecureAllowDumpSos {
|
|
||||||
log.Info(sos)
|
|
||||||
sos = "Your SOS Report has been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpSos: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := &httpbody.HttpBody{
|
|
||||||
ContentType: "text/plain",
|
|
||||||
Data: []byte(sos),
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *apiv1.DumpVarsRequest) (*apiv1.DumpVarsResponse, error) {
|
|
||||||
res := &apiv1.DumpVarsResponse{}
|
|
||||||
|
|
||||||
if !cfg.InsecureAllowDumpVars {
|
|
||||||
res.Alert = "Dumping variables is not allowed by default because it is insecure."
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
|
|
||||||
res.Contents = sv.GetAll()
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *apiv1.DumpPublicIdActionMapRequest) (*apiv1.DumpPublicIdActionMapResponse, error) {
|
|
||||||
res := &apiv1.DumpPublicIdActionMapResponse{}
|
|
||||||
res.Contents = make(map[string]*apiv1.ActionEntityPair)
|
|
||||||
|
|
||||||
if !cfg.InsecureAllowDumpActionMap {
|
|
||||||
res.Alert = "Dumping Public IDs is disallowed."
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
api.executor.MapActionIdToBindingLock.RLock()
|
|
||||||
|
|
||||||
for k, v := range api.executor.MapActionIdToBinding {
|
|
||||||
res.Contents[k] = &apiv1.ActionEntityPair{
|
|
||||||
ActionTitle: v.Action.Title,
|
|
||||||
EntityPrefix: v.EntityPrefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.executor.MapActionIdToBindingLock.RUnlock()
|
|
||||||
|
|
||||||
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *apiv1.GetReadyzRequest) (*apiv1.GetReadyzResponse, error) {
|
|
||||||
res := &apiv1.GetReadyzResponse{
|
|
||||||
Status: "OK",
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start will start the GRPC API.
|
|
||||||
func Start(globalConfig *config.Config, ex *executor.Executor) {
|
|
||||||
cfg = globalConfig
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"address": cfg.ListenAddressGrpcActions,
|
|
||||||
}).Info("Starting gRPC API")
|
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", cfg.ListenAddressGrpcActions)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to listen - %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
grpcServer := grpc.NewServer()
|
|
||||||
apiv1.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
|
|
||||||
|
|
||||||
err = grpcServer.Serve(lis)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not start gRPC Server - %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServer(ex *executor.Executor) *oliveTinAPI {
|
|
||||||
server := oliveTinAPI{}
|
|
||||||
server.executor = ex
|
|
||||||
return &server
|
|
||||||
}
|
|
||||||
|
|
@ -2,20 +2,15 @@ package httpservers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartServers will start 3 HTTP servers. The WebUI, the Rest API, and a proxy
|
// StartServers will start 3 HTTP servers. The WebUI, the Rest API, and a proxy
|
||||||
// for both of them.
|
// for both of them.
|
||||||
func StartServers(cfg *config.Config) {
|
func StartServers(cfg *config.Config, ex *executor.Executor) {
|
||||||
go startWebUIServer(cfg)
|
|
||||||
|
|
||||||
if cfg.UseSingleHTTPFrontend {
|
|
||||||
go StartSingleHTTPFrontend(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Prometheus.Enabled {
|
if cfg.Prometheus.Enabled {
|
||||||
go StartPrometheus(cfg)
|
go StartPrometheus(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
startRestAPIServer(cfg)
|
StartSingleHTTPFrontend(cfg, ex)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
// apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
cors "github.com/OliveTin/OliveTin/internal/cors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func parseHttpHeaderForAuth(cfg *config.Config, req *http.Request) (string, string) {
|
||||||
cfg *config.Config
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseHttpHeaderForAuth(req *http.Request) (string, string) {
|
|
||||||
username, ok := req.Header[cfg.AuthHttpHeaderUsername]
|
username, ok := req.Header[cfg.AuthHttpHeaderUsername]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -49,34 +41,34 @@ func parseHttpHeaderForAuth(req *http.Request) (string, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//gocyclo:ignore
|
//gocyclo:ignore
|
||||||
func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
|
func parseRequestMetadata(cfg *config.Config, ctx context.Context, req *http.Request) metadata.MD {
|
||||||
username := ""
|
username := ""
|
||||||
usergroup := ""
|
usergroup := ""
|
||||||
provider := "unknown"
|
provider := "unknown"
|
||||||
sid := ""
|
sid := ""
|
||||||
|
|
||||||
if cfg.AuthJwtHeader != "" {
|
if cfg.AuthJwtHeader != "" {
|
||||||
username, usergroup = parseJwtHeader(req)
|
username, usergroup = parseJwtHeader(cfg, req)
|
||||||
provider = "jwt-header"
|
provider = "jwt-header"
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.AuthJwtCookieName != "" {
|
if cfg.AuthJwtCookieName != "" {
|
||||||
username, usergroup = parseJwtCookie(req)
|
username, usergroup = parseJwtCookie(cfg, req)
|
||||||
provider = "jwt-cookie"
|
provider = "jwt-cookie"
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.AuthHttpHeaderUsername != "" && username == "" {
|
if cfg.AuthHttpHeaderUsername != "" && username == "" {
|
||||||
username, usergroup = parseHttpHeaderForAuth(req)
|
username, usergroup = parseHttpHeaderForAuth(cfg, req)
|
||||||
provider = "http-header"
|
provider = "http-header"
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.AuthOAuth2Providers) > 0 && username == "" {
|
// if len(cfg.AuthOAuth2Providers) > 0 && username == "" {
|
||||||
username, usergroup, sid = parseOAuth2Cookie(req)
|
// username, usergroup, sid = parseOAuth2Cookie(req)
|
||||||
provider = "oauth2"
|
// provider = "oauth2"
|
||||||
}
|
// }
|
||||||
|
|
||||||
if cfg.AuthLocalUsers.Enabled && username == "" {
|
if cfg.AuthLocalUsers.Enabled && username == "" {
|
||||||
username, usergroup, sid = parseLocalUserCookie(req)
|
username, usergroup, sid = parseLocalUserCookie(cfg, req)
|
||||||
provider = "local"
|
provider = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,12 +84,12 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
|
||||||
return md
|
return md
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtHeader(req *http.Request) (string, string) {
|
func parseJwtHeader(cfg *config.Config, req *http.Request) (string, string) {
|
||||||
// JWTs in the Authorization header are usually prefixed with "Bearer " which is not part of the JWT token.
|
// JWTs in the Authorization header are usually prefixed with "Bearer " which is not part of the JWT token.
|
||||||
return parseJwt(strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
|
return parseJwt(cfg, strings.TrimPrefix(req.Header.Get(cfg.AuthJwtHeader), "Bearer "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
|
func (h *OAuth2Handler) forwardResponseHandler(cfg *config.Config, ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
|
||||||
md, ok := runtime.ServerMetadataFromContext(ctx)
|
md, ok := runtime.ServerMetadataFromContext(ctx)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -105,17 +97,17 @@ func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg prot
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
forwardResponseHandlerLoginLocalUser(md.HeaderMD, w)
|
forwardResponseHandlerLoginLocalUser(cfg, md.HeaderMD, w)
|
||||||
forwardResponseHandlerLogout(md.HeaderMD, w)
|
h.forwardResponseHandlerLogout(cfg, md.HeaderMD, w)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
|
func (h *OAuth2Handler) forwardResponseHandlerLogout(cfg *config.Config, md metadata.MD, w http.ResponseWriter) {
|
||||||
if getMetadataKeyOrEmpty(md, "logout-provider") != "" {
|
if getMetadataKeyOrEmpty(md, "logout-provider") != "" {
|
||||||
sid := getMetadataKeyOrEmpty(md, "logout-sid")
|
sid := getMetadataKeyOrEmpty(md, "logout-sid")
|
||||||
|
|
||||||
delete(registeredStates, sid)
|
delete(h.registeredStates, sid)
|
||||||
http.SetCookie(
|
http.SetCookie(
|
||||||
w,
|
w,
|
||||||
&http.Cookie{
|
&http.Cookie{
|
||||||
|
|
@ -127,7 +119,7 @@ func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
deleteLocalUserSession("local", sid)
|
deleteLocalUserSession(cfg, "local", sid)
|
||||||
|
|
||||||
http.SetCookie(
|
http.SetCookie(
|
||||||
w,
|
w,
|
||||||
|
|
@ -156,52 +148,3 @@ func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalRestConfig(config *config.Config) {
|
|
||||||
cfg = config
|
|
||||||
}
|
|
||||||
|
|
||||||
func startRestAPIServer(globalConfig *config.Config) error {
|
|
||||||
cfg = globalConfig
|
|
||||||
|
|
||||||
loadUserSessions()
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"address": cfg.ListenAddressRestActions,
|
|
||||||
}).Info("Starting REST API")
|
|
||||||
|
|
||||||
mux := newMux()
|
|
||||||
|
|
||||||
return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMux() *runtime.ServeMux {
|
|
||||||
// The MarshalOptions set some important compatibility settings for the webui. See below.
|
|
||||||
mux := runtime.NewServeMux(
|
|
||||||
runtime.WithMetadata(parseRequestMetadata),
|
|
||||||
runtime.WithForwardResponseOption(forwardResponseHandler),
|
|
||||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
|
|
||||||
Marshaler: &runtime.JSONPb{
|
|
||||||
MarshalOptions: protojson.MarshalOptions{
|
|
||||||
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
|
|
||||||
EmitUnpopulated: true, // Emit empty fields so that javascript does not get "undefined" when accessing fields with empty values.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
opts := []grpc.DialOption{
|
|
||||||
grpc.WithTransportCredentials(
|
|
||||||
insecure.NewCredentials(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := apiv1.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Could not register REST API Handler %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mux
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,14 @@ func registerSessionProvider(provider string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteLocalUserSession(provider string, sid string) {
|
func deleteLocalUserSession(cfg *config.Config, provider string, sid string) {
|
||||||
sessionStorageMutex.Lock()
|
sessionStorageMutex.Lock()
|
||||||
|
|
||||||
deleteLocalUserSessionBatch(provider, sid)
|
deleteLocalUserSessionBatch(provider, sid)
|
||||||
|
|
||||||
sessionStorageMutex.Unlock()
|
sessionStorageMutex.Unlock()
|
||||||
|
|
||||||
saveUserSessions()
|
saveUserSessions(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteLocalUserSessionBatch(provider string, sid string) {
|
func deleteLocalUserSessionBatch(provider string, sid string) {
|
||||||
|
|
@ -68,7 +68,7 @@ func deleteLocalUserSessionBatch(provider string, sid string) {
|
||||||
delete(sessionStorage.Providers[provider].Sessions, sid)
|
delete(sessionStorage.Providers[provider].Sessions, sid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerUserSession(provider string, sid string, username string) {
|
func registerUserSession(cfg *config.Config, provider string, sid string, username string) {
|
||||||
sessionStorageMutex.Lock()
|
sessionStorageMutex.Lock()
|
||||||
sessionStorage.Providers[provider].Sessions[sid] = &UserSession{
|
sessionStorage.Providers[provider].Sessions[sid] = &UserSession{
|
||||||
Username: username,
|
Username: username,
|
||||||
|
|
@ -76,10 +76,10 @@ func registerUserSession(provider string, sid string, username string) {
|
||||||
}
|
}
|
||||||
sessionStorageMutex.Unlock()
|
sessionStorageMutex.Unlock()
|
||||||
|
|
||||||
saveUserSessions()
|
saveUserSessions(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveUserSessions() {
|
func saveUserSessions(cfg *config.Config) {
|
||||||
sessionStorageMutex.Lock()
|
sessionStorageMutex.Lock()
|
||||||
defer sessionStorageMutex.Unlock()
|
defer sessionStorageMutex.Unlock()
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ func saveUserSessions() {
|
||||||
filehelper.WriteFile(filename, out)
|
filehelper.WriteFile(filename, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserSessions() {
|
func loadUserSessions(cfg *config.Config) {
|
||||||
registerSessionProviders()
|
registerSessionProviders()
|
||||||
|
|
||||||
filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
|
filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
|
||||||
|
|
@ -124,10 +124,10 @@ func loadUserSessions() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteExpiredSessions()
|
deleteExpiredSessions(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteExpiredSessions() {
|
func deleteExpiredSessions(cfg *config.Config) {
|
||||||
sessionStorageMutex.Lock()
|
sessionStorageMutex.Lock()
|
||||||
|
|
||||||
for provider, sessions := range sessionStorage.Providers {
|
for provider, sessions := range sessionStorage.Providers {
|
||||||
|
|
@ -140,10 +140,10 @@ func deleteExpiredSessions() {
|
||||||
|
|
||||||
sessionStorageMutex.Unlock()
|
sessionStorageMutex.Unlock()
|
||||||
|
|
||||||
saveUserSessions()
|
saveUserSessions(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserFromSession(providerName string, sid string) *config.LocalUser {
|
func getUserFromSession(cfg *config.Config, providerName string, sid string) *config.LocalUser {
|
||||||
provider, ok := sessionStorage.Providers[providerName]
|
provider, ok := sessionStorage.Providers[providerName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
|
||||||
// "github.com/coreos/go-oidc/v3/oidc"
|
// "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/MicahParks/keyfunc/v3"
|
"github.com/MicahParks/keyfunc/v3"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -23,7 +25,7 @@ var (
|
||||||
jwksVerifier keyfunc.Keyfunc
|
jwksVerifier keyfunc.Keyfunc
|
||||||
)
|
)
|
||||||
|
|
||||||
func initJwks() {
|
func initJwks(cfg *config.Config) {
|
||||||
if jwksVerifier == nil {
|
if jwksVerifier == nil {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
|
@ -43,7 +45,7 @@ func initJwks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLocalPublicKey() error {
|
func readLocalPublicKey(cfg *config.Config) error {
|
||||||
if pubKeyBytes != nil {
|
if pubKeyBytes != nil {
|
||||||
return nil // Already read.
|
return nil // Already read.
|
||||||
}
|
}
|
||||||
|
|
@ -62,14 +64,14 @@ func readLocalPublicKey() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtTokenWithRemoteKey(jwtToken string) (*jwt.Token, error) {
|
func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
|
||||||
initJwks()
|
initJwks(cfg)
|
||||||
|
|
||||||
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtTokenWithLocalKey(jwtString string) (*jwt.Token, error) {
|
func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||||
err := readLocalPublicKey()
|
err := readLocalPublicKey(cfg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -85,7 +87,7 @@ func parseJwtTokenWithLocalKey(jwtString string) (*jwt.Token, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash-based Message Authentication Code
|
// Hash-based Message Authentication Code
|
||||||
func parseJwtTokenWithHMAC(jwtString string) (*jwt.Token, error) {
|
func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||||
return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) {
|
return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"])
|
return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"])
|
||||||
|
|
@ -95,20 +97,20 @@ func parseJwtTokenWithHMAC(jwtString string) (*jwt.Token, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtToken(jwtString string) (*jwt.Token, error) {
|
func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||||
if cfg.AuthJwtCertsURL != "" {
|
if cfg.AuthJwtCertsURL != "" {
|
||||||
return parseJwtTokenWithRemoteKey(jwtString)
|
return parseJwtTokenWithRemoteKey(cfg, jwtString)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.AuthJwtPubKeyPath != "" {
|
if cfg.AuthJwtPubKeyPath != "" {
|
||||||
return parseJwtTokenWithLocalKey(jwtString)
|
return parseJwtTokenWithLocalKey(cfg, jwtString)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseJwtTokenWithHMAC(jwtString)
|
return parseJwtTokenWithHMAC(cfg, jwtString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClaimsFromJwtToken(jwtString string) (jwt.MapClaims, error) {
|
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
|
||||||
token, err := parseJwtToken(jwtString)
|
token, err := parseJwtToken(cfg, jwtString)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("jwt parse failure: %v", err)
|
log.Errorf("jwt parse failure: %v", err)
|
||||||
|
|
@ -130,7 +132,7 @@ func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtCookie(request *http.Request) (string, string) {
|
func parseJwtCookie(cfg *config.Config, request *http.Request) (string, string) {
|
||||||
cookie, err := request.Cookie(cfg.AuthJwtCookieName)
|
cookie, err := request.Cookie(cfg.AuthJwtCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -138,11 +140,11 @@ func parseJwtCookie(request *http.Request) (string, string) {
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseJwt(cookie.Value)
|
return parseJwt(cfg, cookie.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwt(token string) (string, string) {
|
func parseJwt(cfg *config.Config, token string) (string, string) {
|
||||||
claims, err := getClaimsFromJwtToken(token)
|
claims, err := getClaimsFromJwtToken(cfg, token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("jwt claim error: %+v", err)
|
log.Warnf("jwt claim error: %+v", err)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ package httpservers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
func parseLocalUserCookie(cfg *config.Config, req *http.Request) (string, string, string) {
|
||||||
cookie, err := req.Cookie("olivetin-sid-local")
|
cookie, err := req.Cookie("olivetin-sid-local")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -16,7 +17,7 @@ func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
||||||
|
|
||||||
cookieValue := cookie.Value
|
cookieValue := cookie.Value
|
||||||
|
|
||||||
user := getUserFromSession("local", cookieValue)
|
user := getUserFromSession(cfg, "local", cookieValue)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return "", "", ""
|
return "", "", ""
|
||||||
|
|
@ -25,7 +26,7 @@ func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
||||||
return user.Username, user.Usergroup, cookie.Value
|
return user.Username, user.Usergroup, cookie.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter) error {
|
func forwardResponseHandlerLoginLocalUser(cfg *config.Config, md metadata.MD, w http.ResponseWriter) error {
|
||||||
setUsername := getMetadataKeyOrEmpty(md, "set-username")
|
setUsername := getMetadataKeyOrEmpty(md, "set-username")
|
||||||
|
|
||||||
if setUsername != "" {
|
if setUsername != "" {
|
||||||
|
|
@ -36,7 +37,7 @@ func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
sid := uuid.NewString()
|
sid := uuid.NewString()
|
||||||
registerUserSession("local", sid, user.Username)
|
registerUserSession(cfg, "local", sid, user.Username)
|
||||||
|
|
||||||
http.SetCookie(
|
http.SetCookie(
|
||||||
w,
|
w,
|
||||||
|
|
|
||||||
|
|
@ -17,25 +17,20 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type OAuth2Handler struct {
|
||||||
registeredStates = make(map[string]*oauth2State)
|
cfg *config.Config
|
||||||
registeredProviders = make(map[string]*oauth2.Config)
|
registeredStates map[string]*oauth2State
|
||||||
)
|
registeredProviders map[string]*oauth2.Config
|
||||||
|
|
||||||
type oauth2State struct {
|
|
||||||
providerConfig *oauth2.Config
|
|
||||||
providerName string
|
|
||||||
Username string
|
|
||||||
Usergroup string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func assignIfEmpty(target *string, value string) {
|
func NewOAuth2Handler(cfg *config.Config) *OAuth2Handler {
|
||||||
if *target == "" {
|
h := &OAuth2Handler{
|
||||||
*target = value
|
cfg: cfg,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func oauth2Init(cfg *config.Config) {
|
h.registeredStates = make(map[string]*oauth2State)
|
||||||
|
h.registeredProviders = make(map[string]*oauth2.Config)
|
||||||
|
|
||||||
for providerName, providerConfig := range cfg.AuthOAuth2Providers {
|
for providerName, providerConfig := range cfg.AuthOAuth2Providers {
|
||||||
completeProviderConfig(providerName, providerConfig)
|
completeProviderConfig(providerName, providerConfig)
|
||||||
|
|
||||||
|
|
@ -50,10 +45,25 @@ func oauth2Init(cfg *config.Config) {
|
||||||
RedirectURL: cfg.AuthOAuth2RedirectURL,
|
RedirectURL: cfg.AuthOAuth2RedirectURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredProviders[providerName] = newConfig
|
h.registeredProviders[providerName] = newConfig
|
||||||
|
|
||||||
log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
|
log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type oauth2State struct {
|
||||||
|
providerConfig *oauth2.Config
|
||||||
|
providerName string
|
||||||
|
Username string
|
||||||
|
Usergroup string
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignIfEmpty(target *string, value string) {
|
||||||
|
if *target == "" {
|
||||||
|
*target = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
|
func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
|
||||||
|
|
@ -76,8 +86,8 @@ func completeProviderConfig(providerName string, providerConfig *config.OAuth2Pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOAuth2Config(providerName string) (*oauth2.Config, error) {
|
func (h *OAuth2Handler) getOAuth2Config(providerName string) (*oauth2.Config, error) {
|
||||||
config, ok := registeredProviders[providerName]
|
config, ok := h.registeredProviders[providerName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("provider not found in config: %v", providerName)
|
return nil, fmt.Errorf("provider not found in config: %v", providerName)
|
||||||
|
|
@ -96,7 +106,7 @@ func randString(nByte int) (string, error) {
|
||||||
return base64.URLEncoding.EncodeToString(b), nil
|
return base64.URLEncoding.EncodeToString(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
|
func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
|
|
@ -109,7 +119,7 @@ func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
func (h *OAuth2Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
state, err := randString(16)
|
state, err := randString(16)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -118,7 +128,7 @@ func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
providerName := r.URL.Query().Get("provider")
|
providerName := r.URL.Query().Get("provider")
|
||||||
provider, err := getOAuth2Config(providerName)
|
provider, err := h.getOAuth2Config(providerName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to get provider config: %v %v", providerName, err)
|
log.Errorf("Failed to get provider config: %v %v", providerName, err)
|
||||||
|
|
@ -126,20 +136,20 @@ func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredStates[state] = &oauth2State{
|
h.registeredStates[state] = &oauth2State{
|
||||||
providerConfig: provider,
|
providerConfig: provider,
|
||||||
providerName: providerName,
|
providerName: providerName,
|
||||||
Username: "",
|
Username: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
|
h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
|
||||||
|
|
||||||
log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
|
log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
|
||||||
|
|
||||||
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
|
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
||||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||||
state := cookie.Value
|
state := cookie.Value
|
||||||
|
|
||||||
|
|
@ -157,7 +167,7 @@ func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2St
|
||||||
return nil, state, false
|
return nil, state, false
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredState, ok := registeredStates[state]
|
registeredState, ok := h.registeredStates[state]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("State not found in server: %v", state)
|
log.Errorf("State not found in server: %v", state)
|
||||||
|
|
@ -206,10 +216,10 @@ func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
|
||||||
return caCertPool
|
return caCertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
func (h *OAuth2Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Infof("OAuth2 Callback received")
|
log.Infof("OAuth2 Callback received")
|
||||||
|
|
||||||
registeredState, state, ok := checkOAuthCallbackCookie(w, r)
|
registeredState, state, ok := h.checkOAuthCallbackCookie(w, r)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
|
@ -222,7 +232,7 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
"token-code": code,
|
"token-code": code,
|
||||||
}).Debug("OAuth2 Token Code")
|
}).Debug("OAuth2 Token Code")
|
||||||
|
|
||||||
providerConfig := cfg.AuthOAuth2Providers[registeredState.providerName]
|
providerConfig := h.cfg.AuthOAuth2Providers[registeredState.providerName]
|
||||||
|
|
||||||
clientSettings := getOAuth2HttpClient(providerConfig)
|
clientSettings := getOAuth2HttpClient(providerConfig)
|
||||||
|
|
||||||
|
|
@ -250,18 +260,18 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
Timeout: clientSettings.Timeout,
|
Timeout: clientSettings.Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
userinfo := getUserInfo(userInfoClient, cfg.AuthOAuth2Providers[registeredState.providerName])
|
userinfo := getUserInfo(userInfoClient, h.cfg.AuthOAuth2Providers[registeredState.providerName])
|
||||||
|
|
||||||
registeredStates[state].Username = userinfo.Username
|
h.registeredStates[state].Username = userinfo.Username
|
||||||
registeredStates[state].Usergroup = userinfo.Usergroup
|
h.registeredStates[state].Usergroup = userinfo.Usergroup
|
||||||
|
|
||||||
for k, v := range registeredStates {
|
for k, v := range h.registeredStates {
|
||||||
log.Debugf("states: %+v %+v", k, v)
|
log.Debugf("states: %+v %+v", k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"state": state,
|
"state": state,
|
||||||
"username": registeredStates[state].Username,
|
"username": h.registeredStates[state].Username,
|
||||||
}).Info("OAuth2 login successful")
|
}).Info("OAuth2 login successful")
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
|
@ -330,7 +340,7 @@ func getDataField(data map[string]any, field string) string {
|
||||||
return val.(string)
|
return val.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOAuth2Cookie(r *http.Request) (string, string, string) {
|
func (h *OAuth2Handler) parseOAuth2Cookie(r *http.Request) (string, string, string) {
|
||||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -342,7 +352,7 @@ func parseOAuth2Cookie(r *http.Request) (string, string, string) {
|
||||||
return "", "", ""
|
return "", "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
serverState, found := registeredStates[cookie.Value]
|
serverState, found := h.registeredStates[cookie.Value]
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ away, and several other issues.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/websocket"
|
"github.com/OliveTin/OliveTin/internal/api"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
|
func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
|
||||||
|
|
@ -31,42 +33,40 @@ func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
|
||||||
|
|
||||||
// StartSingleHTTPFrontend will create a reverse proxy that proxies the API
|
// StartSingleHTTPFrontend will create a reverse proxy that proxies the API
|
||||||
// and webui internally.
|
// and webui internally.
|
||||||
func StartSingleHTTPFrontend(cfg *config.Config) {
|
func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"address": cfg.ListenAddressSingleHTTPFrontend,
|
"address": cfg.ListenAddressSingleHTTPFrontend,
|
||||||
}).Info("Starting single HTTP frontend")
|
}).Info("Starting single HTTP frontend")
|
||||||
|
|
||||||
apiURL, _ := url.Parse("http://" + cfg.ListenAddressRestActions)
|
|
||||||
apiProxy := httputil.NewSingleHostReverseProxy(apiURL)
|
|
||||||
|
|
||||||
webuiURL, _ := url.Parse("http://" + cfg.ListenAddressWebUI)
|
|
||||||
webuiProxy := httputil.NewSingleHostReverseProxy(webuiURL)
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
apiPath, apiHandler := api.GetNewHandler(ex)
|
||||||
logDebugRequest(cfg, "api ", r)
|
|
||||||
|
|
||||||
apiProxy.ServeHTTP(w, r)
|
log.Infof("API path is %s", apiPath)
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/api/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
logDebugRequest(cfg, "ws ", r)
|
fn := path.Base(r.URL.Path)
|
||||||
|
|
||||||
websocket.HandleWebsocket(w, r)
|
r.URL.Path = apiPath + fn
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("/oauth/login", handleOAuthLogin)
|
log.Infof("SingleFrontend HTTP API Req URL after rewrite: %v", r.URL.Path)
|
||||||
|
|
||||||
mux.HandleFunc("/oauth/callback", handleOAuthCallback)
|
apiHandler.ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
oauth2handler := NewOAuth2Handler(cfg)
|
||||||
|
|
||||||
|
mux.HandleFunc("/oauth/login", oauth2handler.handleOAuthLogin)
|
||||||
|
mux.HandleFunc("/oauth/callback", oauth2handler.handleOAuthCallback)
|
||||||
|
|
||||||
mux.HandleFunc("/readyz", handleReadyz)
|
mux.HandleFunc("/readyz", handleReadyz)
|
||||||
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
webuiServer := NewWebUIServer(cfg)
|
||||||
logDebugRequest(cfg, "ui ", r)
|
|
||||||
|
|
||||||
webuiProxy.ServeHTTP(w, r)
|
mux.HandleFunc("/webUiSettings.json", webuiServer.generateWebUISettings)
|
||||||
})
|
mux.HandleFunc("/theme.css", webuiServer.generateThemeCss)
|
||||||
|
mux.Handle("/custom-webui/", webuiServer.handleCustomWebui())
|
||||||
|
mux.HandleFunc("/", webuiServer.handleWebui)
|
||||||
|
|
||||||
if cfg.Prometheus.Enabled {
|
if cfg.Prometheus.Enabled {
|
||||||
promURL, _ := url.Parse("http://" + cfg.ListenAddressPrometheus)
|
promURL, _ := url.Parse("http://" + cfg.ListenAddressPrometheus)
|
||||||
|
|
@ -79,8 +79,6 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth2Init(cfg)
|
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: cfg.ListenAddressSingleHTTPFrontend,
|
Addr: cfg.ListenAddressSingleHTTPFrontend,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,20 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
|
"github.com/jamesread/golure/pkg/dirs"
|
||||||
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type webUIServer struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
webuiDir string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
customThemeCss []byte
|
customThemeCss []byte
|
||||||
customThemeCssRead = false
|
customThemeCssRead = false
|
||||||
|
|
@ -36,46 +43,65 @@ type webUISettings struct {
|
||||||
AdditionalLinks []*config.NavigationLink
|
AdditionalLinks []*config.NavigationLink
|
||||||
}
|
}
|
||||||
|
|
||||||
func findWebuiDir() string {
|
func NewWebUIServer(cfg *config.Config) *webUIServer {
|
||||||
|
s := &webUIServer{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.webuiDir = s.findWebuiDir()
|
||||||
|
s.setupCustomWebuiDir()
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//dirName := path.Dir(r.URL.Path)
|
||||||
|
|
||||||
|
// Mangle requests for any path like /logs or /config to load the webui index.html
|
||||||
|
if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
|
||||||
|
log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
|
||||||
|
} else {
|
||||||
|
log.Infof("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
|
||||||
|
http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
|
||||||
|
// http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *webUIServer) findWebuiDir() string {
|
||||||
directoriesToSearch := []string{
|
directoriesToSearch := []string{
|
||||||
cfg.WebUIDir,
|
s.cfg.WebUIDir,
|
||||||
"../webui/",
|
"../frontend/dist/",
|
||||||
"/usr/share/OliveTin/webui/",
|
"../frontend/",
|
||||||
|
"/usr/share/OliveTin/frontend/",
|
||||||
"/var/www/OliveTin/",
|
"/var/www/OliveTin/",
|
||||||
"/var/www/olivetin/",
|
"/var/www/olivetin/",
|
||||||
"/etc/OliveTin/webui/",
|
"/etc/OliveTin/frontend/",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a classic i := 0 style for loop here instead of range, as the
|
dir, err := dirs.GetFirstExistingDirectory("webui", directoriesToSearch)
|
||||||
// search order must be deterministic - the order that the slice was defined in.
|
|
||||||
for i := 0; i < len(directoriesToSearch); i++ {
|
|
||||||
dir := directoriesToSearch[i]
|
|
||||||
absdir, _ := filepath.Abs(dir)
|
|
||||||
|
|
||||||
if _, err := os.Stat(absdir); !os.IsNotExist(err) {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"dir": absdir,
|
|
||||||
}).Infof("Found the webui directory")
|
|
||||||
|
|
||||||
sv.Set("internal.webuidir", absdir+" ("+dir+")")
|
|
||||||
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
log.Warnf("Did not find the webui directory, you will probably get 404 errors.")
|
log.Warnf("Did not find the webui directory, you will probably get 404 errors.")
|
||||||
|
|
||||||
return "./webui" // Should not exist
|
return "./webui" // Should not exist
|
||||||
}
|
}
|
||||||
|
|
||||||
func findCustomWebuiDir() string {
|
log.Infof("Using webui directory: %s", dir)
|
||||||
dir := path.Join(cfg.GetDir(), "custom-webui")
|
|
||||||
|
|
||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCustomWebuiDir() {
|
func (s *webUIServer) findCustomWebuiDir() string {
|
||||||
dir := findCustomWebuiDir()
|
dir := path.Join(s.cfg.GetDir(), "custom-webui")
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webUIServer) setupCustomWebuiDir() {
|
||||||
|
dir := s.findCustomWebuiDir()
|
||||||
|
|
||||||
err := os.MkdirAll(path.Join(dir, "themes/"), 0775)
|
err := os.MkdirAll(path.Join(dir, "themes/"), 0775)
|
||||||
|
|
||||||
|
|
@ -87,10 +113,10 @@ func setupCustomWebuiDir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
func (s *webUIServer) generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
||||||
themeCssFilename := path.Join(findCustomWebuiDir(), "themes", cfg.ThemeName, "theme.css")
|
themeCssFilename := path.Join(s.findCustomWebuiDir(), "themes", s.cfg.ThemeName, "theme.css")
|
||||||
|
|
||||||
if !customThemeCssRead || cfg.ThemeCacheDisabled {
|
if !customThemeCssRead || s.cfg.ThemeCacheDisabled {
|
||||||
customThemeCssRead = true
|
customThemeCssRead = true
|
||||||
|
|
||||||
if _, err := os.Stat(themeCssFilename); err == nil {
|
if _, err := os.Stat(themeCssFilename); err == nil {
|
||||||
|
|
@ -125,22 +151,24 @@ func buildPublicOAuth2ProvidersList(cfg *config.Config) []publicOAuth2Provider {
|
||||||
return publicProviders
|
return publicProviders
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
|
func (s *webUIServer) generateWebUISettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Infof("Generating webui settings for %s", r.RemoteAddr)
|
||||||
|
|
||||||
jsonRet, _ := json.Marshal(webUISettings{
|
jsonRet, _ := json.Marshal(webUISettings{
|
||||||
Rest: cfg.ExternalRestAddress + "/api/",
|
Rest: s.cfg.ExternalRestAddress + "/api/",
|
||||||
ShowFooter: cfg.ShowFooter,
|
ShowFooter: s.cfg.ShowFooter,
|
||||||
ShowNavigation: cfg.ShowNavigation,
|
ShowNavigation: s.cfg.ShowNavigation,
|
||||||
ShowNewVersions: cfg.ShowNewVersions,
|
ShowNewVersions: s.cfg.ShowNewVersions,
|
||||||
AvailableVersion: installationinfo.Runtime.AvailableVersion,
|
AvailableVersion: installationinfo.Runtime.AvailableVersion,
|
||||||
CurrentVersion: installationinfo.Build.Version,
|
CurrentVersion: installationinfo.Build.Version,
|
||||||
PageTitle: cfg.PageTitle,
|
PageTitle: s.cfg.PageTitle,
|
||||||
SectionNavigationStyle: cfg.SectionNavigationStyle,
|
SectionNavigationStyle: s.cfg.SectionNavigationStyle,
|
||||||
DefaultIconForBack: cfg.DefaultIconForBack,
|
DefaultIconForBack: s.cfg.DefaultIconForBack,
|
||||||
EnableCustomJs: cfg.EnableCustomJs,
|
EnableCustomJs: s.cfg.EnableCustomJs,
|
||||||
AuthLoginUrl: cfg.AuthLoginUrl,
|
AuthLoginUrl: s.cfg.AuthLoginUrl,
|
||||||
AuthLocalLogin: cfg.AuthLocalUsers.Enabled,
|
AuthLocalLogin: s.cfg.AuthLocalUsers.Enabled,
|
||||||
AuthOAuth2Providers: buildPublicOAuth2ProvidersList(cfg),
|
AuthOAuth2Providers: buildPublicOAuth2ProvidersList(s.cfg),
|
||||||
AdditionalLinks: cfg.AdditionalNavigationLinks,
|
AdditionalLinks: s.cfg.AdditionalNavigationLinks,
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
|
@ -151,36 +179,6 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startWebUIServer(cfg *config.Config) {
|
func (s *webUIServer) handleCustomWebui() (http.Handler) {
|
||||||
log.WithFields(log.Fields{
|
return http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(s.findCustomWebuiDir())))
|
||||||
"address": cfg.ListenAddressWebUI,
|
|
||||||
}).Info("Starting WebUI server")
|
|
||||||
|
|
||||||
setupCustomWebuiDir()
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.Handle("/custom-webui/", http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(findCustomWebuiDir()))))
|
|
||||||
mux.HandleFunc("/theme.css", generateThemeCss)
|
|
||||||
mux.HandleFunc("/webUiSettings.json", generateWebUISettings)
|
|
||||||
|
|
||||||
webuiDir := findWebuiDir()
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
dirName := path.Dir(r.URL.Path)
|
|
||||||
|
|
||||||
// Mangle requests for any path like /logs or /config to load the webui index.html
|
|
||||||
if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
|
|
||||||
log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
|
|
||||||
|
|
||||||
http.ServeFile(w, r, path.Join(webuiDir, "index.html"))
|
|
||||||
} else {
|
|
||||||
http.StripPrefix(dirName, http.FileServer(http.Dir(webuiDir))).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: cfg.ListenAddressWebUI,
|
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Fatal(srv.ListenAndServe())
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
ws "github.com/gorilla/websocket"
|
ws "github.com/gorilla/websocket"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
|
|
||||||
"github.com/OliveTin/OliveTin/internal/entityfiles"
|
"github.com/OliveTin/OliveTin/internal/entityfiles"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
|
|
||||||
"github.com/OliveTin/OliveTin/internal/httpservers"
|
"github.com/OliveTin/OliveTin/internal/httpservers"
|
||||||
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||||
"github.com/OliveTin/OliveTin/internal/oncalendarfile"
|
"github.com/OliveTin/OliveTin/internal/oncalendarfile"
|
||||||
|
|
@ -179,7 +178,5 @@ func main() {
|
||||||
|
|
||||||
go updatecheck.StartUpdateChecker(cfg)
|
go updatecheck.StartUpdateChecker(cfg)
|
||||||
|
|
||||||
go grpcapi.Start(cfg, executor)
|
httpservers.StartServers(cfg, executor)
|
||||||
|
|
||||||
httpservers.StartServers(cfg)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import (
|
||||||
_ "github.com/bufbuild/buf/cmd/buf"
|
_ "github.com/bufbuild/buf/cmd/buf"
|
||||||
_ "github.com/fzipp/gocyclo/cmd/gocyclo"
|
_ "github.com/fzipp/gocyclo/cmd/gocyclo"
|
||||||
_ "github.com/go-critic/go-critic/cmd/gocritic"
|
_ "github.com/go-critic/go-critic/cmd/gocritic"
|
||||||
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
|
|
||||||
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
|
|
||||||
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||||
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
- id: kitchen
|
||||||
|
name: Kitchen
|
||||||
|
turned_on: false
|
||||||
|
- id: living_room
|
||||||
|
name: Living Room
|
||||||
|
turned_on: false
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="100mm"
|
||||||
|
height="100mm"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
sodipodi:docname="OliveTinLogoMonocrome.svg"
|
||||||
|
inkscape:export-filename="/home/xconspirisist/sandbox/OliveTin/webui/OliveTinLogo.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1"
|
||||||
|
inkscape:cx="214.5"
|
||||||
|
inkscape:cy="130.5"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1056"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="1104"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
id="rect1402"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.79097;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 78.150303,10.636649 0.06022,77.69053 c 0,0 -0.333558,7.417739 -26.071829,7.668294 -25.738273,0.250554 -27.812975,-7.668294 -27.812975,-7.668294 l 0.02958,-77.69053"
|
||||||
|
sodipodi:nodetypes="cczcc" />
|
||||||
|
<path
|
||||||
|
id="rect1469-7"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 26.974,41.794388 23.923879,2.635333 24.529783,-2.635333 v 21.176145 c 0,0 -0.404835,4.427058 -24.431009,4.612696 C 26.97048,67.768868 26.974,62.970533 26.974,62.970533 Z"
|
||||||
|
sodipodi:nodetypes="cccczcc" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:4.00217;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path1436"
|
||||||
|
ry="19.291208"
|
||||||
|
rx="13.695867"
|
||||||
|
cy="28.679092"
|
||||||
|
cx="71.078308"
|
||||||
|
transform="rotate(25.917116)" />
|
||||||
|
<ellipse
|
||||||
|
style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.79097;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path1440"
|
||||||
|
cx="51.2528"
|
||||||
|
cy="10.636649"
|
||||||
|
rx="26.897503"
|
||||||
|
ry="6.525641" />
|
||||||
|
<path
|
||||||
|
id="path1436-3"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.574412;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
inkscape:transform-center-x="-1.1166908"
|
||||||
|
inkscape:transform-center-y="0.49206772"
|
||||||
|
d="m 36.494243,8.7784525 c 1.841786,-0.1158042 2.469235,0.5295096 2.656071,1.4191125 0.186835,0.889603 -0.0974,1.788639 -1.939188,1.904443 -0.848904,0.05338 -2.422996,-0.462298 -3.064668,-0.677573 -0.750505,-0.251787 -1.565122,-0.327933 -1.665841,-0.807506 -0.09913,-0.472029 0.614422,-0.7401021 1.22232,-1.0768269 0.537767,-0.2978804 1.926784,-0.7072919 2.791306,-0.7616496 z"
|
||||||
|
sodipodi:nodetypes="sssssss" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@parcel/config-default",
|
|
||||||
"resolvers": ["parcel-resolver-ignore", "..."]
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
codestyle:
|
|
||||||
npm install
|
|
||||||
npx eslint --fix main.js js/*
|
|
||||||
npx stylelint style.css
|
|
||||||
|
|
||||||
.PHONY: codestyle
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
import './ExecutionButton.js'
|
|
||||||
import './ArgumentForm.js'
|
|
||||||
import { ExecutionFeedbackButton } from './ExecutionFeedbackButton.js'
|
|
||||||
|
|
||||||
class ActionButton extends ExecutionFeedbackButton {
|
|
||||||
constructDomFromTemplate () {
|
|
||||||
const tpl = document.getElementById('tplActionButton')
|
|
||||||
const content = tpl.content.cloneNode(true)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* FIXME: Should probably be using a shadowdom here, but seem to
|
|
||||||
* get an error when combined with custom elements.
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.appendChild(content)
|
|
||||||
|
|
||||||
this.btn = this.querySelector('button')
|
|
||||||
this.domTitle = this.btn.querySelector('.title')
|
|
||||||
this.domIcon = this.btn.querySelector('.icon')
|
|
||||||
}
|
|
||||||
|
|
||||||
constructFromJson (json) {
|
|
||||||
this.updateIterationTimestamp = 0
|
|
||||||
|
|
||||||
this.constructDomFromTemplate()
|
|
||||||
|
|
||||||
// Class attributes
|
|
||||||
this.updateFromJson(json)
|
|
||||||
|
|
||||||
this.actionId = json.id
|
|
||||||
|
|
||||||
// DOM Attributes
|
|
||||||
this.setAttribute('role', 'none')
|
|
||||||
this.setAttribute('id', 'actionButton-' + this.actionId)
|
|
||||||
|
|
||||||
if (!json.canExec) {
|
|
||||||
this.btn.disabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.btn.setAttribute('id', 'actionButtonInner-' + this.actionId)
|
|
||||||
this.btn.title = json.title
|
|
||||||
this.btn.onclick = () => {
|
|
||||||
if (json.arguments.length > 0) {
|
|
||||||
for (const oldArgumentForm of document.querySelectorAll('argument-form')) {
|
|
||||||
oldArgumentForm.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateUrlWithAction()
|
|
||||||
|
|
||||||
const frm = document.createElement('argument-form')
|
|
||||||
frm.setup(json, (args) => {
|
|
||||||
this.startAction(args)
|
|
||||||
})
|
|
||||||
|
|
||||||
document.body.appendChild(frm)
|
|
||||||
frm.querySelector('dialog').showModal()
|
|
||||||
} else {
|
|
||||||
this.startAction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.popupOnStart = json.popupOnStart
|
|
||||||
|
|
||||||
this.updateFromJson(json)
|
|
||||||
|
|
||||||
this.domTitle.innerText = this.btn.title
|
|
||||||
this.domIcon.innerHTML = this.unicodeIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFromJson (json) {
|
|
||||||
// Fields that should not be updated
|
|
||||||
//
|
|
||||||
// title - as the callback URL relies on it
|
|
||||||
|
|
||||||
if (json.icon === '') {
|
|
||||||
this.unicodeIcon = '💩'
|
|
||||||
} else {
|
|
||||||
this.unicodeIcon = unescape(json.icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.domIcon.innerHTML = this.unicodeIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
onExecStatusChanged () {
|
|
||||||
this.btn.disabled = false
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateDom(null, this.btn.title)
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUniqueId () {
|
|
||||||
if (window.isSecureContext) {
|
|
||||||
return window.crypto.randomUUID()
|
|
||||||
} else {
|
|
||||||
return Date.now().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startAction (actionArgs) {
|
|
||||||
this.btn.classList = [] // Removes old animation classes
|
|
||||||
|
|
||||||
if (actionArgs === undefined) {
|
|
||||||
actionArgs = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// UUIDs are create client side, so that we can setup a "execution-button"
|
|
||||||
// to track the execution before we send the request to the server.
|
|
||||||
const startActionArgs = {
|
|
||||||
actionId: this.actionId,
|
|
||||||
arguments: actionArgs,
|
|
||||||
uniqueTrackingId: this.getUniqueId()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onActionStarted(startActionArgs.uniqueTrackingId)
|
|
||||||
|
|
||||||
window.fetch(window.restBaseUrl + 'StartAction', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(startActionArgs)
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json()
|
|
||||||
} else {
|
|
||||||
throw new Error(res.statusText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).then((json) => {
|
|
||||||
// The button used to wait for the action to finish, but now it is fire & forget
|
|
||||||
}).catch(err => {
|
|
||||||
throw err // We used to flash buttons red, but now hand to the global error handler
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUrlWithAction () {
|
|
||||||
// Get the current URL and create a new URL object
|
|
||||||
const url = new URL(window.location.href)
|
|
||||||
|
|
||||||
// Set the action parameter
|
|
||||||
url.searchParams.set('action', this.btn.title)
|
|
||||||
|
|
||||||
// Update the URL without reloading the page
|
|
||||||
window.history.replaceState({}, '', url.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
onActionStarted (executionTrackingId) {
|
|
||||||
if (this.popupOnStart === 'execution-button') {
|
|
||||||
const btnExecution = document.createElement('execution-button')
|
|
||||||
btnExecution.constructFromJson(executionTrackingId)
|
|
||||||
this.querySelector('.action-button-footer').hidden = false
|
|
||||||
this.querySelector('.action-button-footer').style.display = 'flex'
|
|
||||||
this.querySelector('.action-button-footer').prepend(btnExecution)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.popupOnStart.includes('execution-dialog')) {
|
|
||||||
window.executionDialog.reset()
|
|
||||||
|
|
||||||
if (this.popupOnStart === 'execution-dialog-stdout-only') {
|
|
||||||
window.executionDialog.hideEverythingApartFromOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.executionDialog.executionTrackingId = executionTrackingId
|
|
||||||
window.executionDialog.show(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.btn.disabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.customElements.define('action-button', ActionButton)
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
export class ActionStatusDisplay {
|
|
||||||
constructor (parentElement) {
|
|
||||||
this.exitCodeElement = document.createElement('span')
|
|
||||||
this.statusElement = document.createElement('span')
|
|
||||||
this.statusElement.innerText = 'unknown'
|
|
||||||
|
|
||||||
parentElement.innerText = ''
|
|
||||||
parentElement.appendChild(this.statusElement)
|
|
||||||
parentElement.appendChild(this.exitCodeElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
getText () {
|
|
||||||
return this.statusElement.innerText
|
|
||||||
}
|
|
||||||
|
|
||||||
update (logEntry) {
|
|
||||||
this.statusElement.classList.remove(...this.statusElement.classList)
|
|
||||||
this.statusElement.classList.add('action-status')
|
|
||||||
|
|
||||||
if (logEntry.executionFinished) {
|
|
||||||
this.statusElement.innerText = 'Completed'
|
|
||||||
this.exitCodeElement.innerText = ' Exit code: ' + logEntry.exitCode
|
|
||||||
|
|
||||||
if (logEntry.exitCode === 0) {
|
|
||||||
this.statusElement.classList.add('action-success')
|
|
||||||
} else if (logEntry.blocked) {
|
|
||||||
this.statusElement.innerText = 'Blocked'
|
|
||||||
this.statusElement.classList.add('action-blocked')
|
|
||||||
this.exitCodeElement.innerText = ''
|
|
||||||
} else if (logEntry.timedOut) {
|
|
||||||
this.statusElement.innerText = 'Timed out'
|
|
||||||
this.statusElement.classList.add('action-timeout')
|
|
||||||
this.exitCodeElement.innerText = ''
|
|
||||||
} else {
|
|
||||||
this.statusElement.classList.add('action-nonzero-exit')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.statusElement.innerText = 'Still running...'
|
|
||||||
this.exitCodeElement.innerText = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { ExecutionFeedbackButton } from './ExecutionFeedbackButton.js'
|
|
||||||
|
|
||||||
class ExecutionButton extends ExecutionFeedbackButton {
|
|
||||||
constructFromJson (json) {
|
|
||||||
this.executionTrackingId = json
|
|
||||||
this.ellapsed = 0
|
|
||||||
|
|
||||||
this.appendChild(document.createElement('button'))
|
|
||||||
this.isWaiting = true
|
|
||||||
|
|
||||||
this.setAttribute('id', 'execution-' + json)
|
|
||||||
|
|
||||||
this.btn = this.querySelector('button')
|
|
||||||
this.btn.innerText = 'Executing...'
|
|
||||||
this.btn.onclick = () => {
|
|
||||||
this.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.domTitle = this.btn
|
|
||||||
}
|
|
||||||
|
|
||||||
show () {
|
|
||||||
window.executionDialog.reset()
|
|
||||||
window.executionDialog.show()
|
|
||||||
window.executionDialog.fetchExecutionResult(this.executionTrackingId)
|
|
||||||
}
|
|
||||||
|
|
||||||
onExecStatusChanged () {
|
|
||||||
this.domTitle.innerText = this.ellapsed + 's'
|
|
||||||
this.btn.title = this.ellapsed + ' seconds'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.customElements.define('execution-button', ExecutionButton)
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
import { ActionStatusDisplay } from './ActionStatusDisplay.js'
|
|
||||||
import { OutputTerminal } from './OutputTerminal.js'
|
|
||||||
|
|
||||||
// This ExecutionDialog is NOT a custom HTML element, but rather just picks up
|
|
||||||
// the <dialog /> element out of index.html and just re-uses that - as only
|
|
||||||
// one dialog can be shown at a time.
|
|
||||||
export class ExecutionDialog {
|
|
||||||
constructor () {
|
|
||||||
this.dlg = document.querySelector('dialog#execution-results-popup')
|
|
||||||
|
|
||||||
this.domIcon = document.getElementById('execution-dialog-icon')
|
|
||||||
this.domTitle = document.getElementById('execution-dialog-title')
|
|
||||||
this.domOutput = document.getElementById('execution-dialog-xterm')
|
|
||||||
this.domOutputHtml = document.getElementById('execution-dialog-output-html')
|
|
||||||
this.domOutputToggleBig = document.getElementById('execution-dialog-toggle-size')
|
|
||||||
this.domOutputToggleBig.onclick = () => {
|
|
||||||
this.toggleSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.domBtnRerun = document.getElementById('execution-dialog-rerun-action')
|
|
||||||
this.domBtnKill = document.getElementById('execution-dialog-kill-action')
|
|
||||||
|
|
||||||
this.domDuration = document.getElementById('execution-dialog-duration')
|
|
||||||
this.domStatus = new ActionStatusDisplay(document.getElementById('execution-dialog-status'))
|
|
||||||
|
|
||||||
this.domExecutionBasics = document.getElementById('execution-dialog-basics')
|
|
||||||
this.domExecutionDetails = document.getElementById('execution-dialog-details')
|
|
||||||
|
|
||||||
window.terminal = new OutputTerminal()
|
|
||||||
window.terminal.open(this.domOutput)
|
|
||||||
window.terminal.resize(80, 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSize () {
|
|
||||||
if (this.dlg.classList.contains('big')) {
|
|
||||||
this.dlg.classList.remove('big')
|
|
||||||
window.terminal.resize(80, 24)
|
|
||||||
} else {
|
|
||||||
this.dlg.classList.add('big')
|
|
||||||
window.terminal.fit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reset () {
|
|
||||||
this.executionSeconds = 0
|
|
||||||
this.executionTrackingId = 'notset'
|
|
||||||
|
|
||||||
this.dlg.classList.remove('big')
|
|
||||||
|
|
||||||
this.domOutputToggleBig.hidden = false
|
|
||||||
|
|
||||||
this.domIcon.innerText = ''
|
|
||||||
this.domTitle.innerText = 'Waiting for result... '
|
|
||||||
this.domDuration.innerText = ''
|
|
||||||
|
|
||||||
// window.terminal.close()
|
|
||||||
|
|
||||||
this.domBtnRerun.disabled = true
|
|
||||||
this.domBtnRerun.onclick = () => {}
|
|
||||||
this.domBtnKill.disabled = true
|
|
||||||
this.domBtnKill.onclick = () => {}
|
|
||||||
|
|
||||||
this.hideDetailsOnResult = false
|
|
||||||
this.domExecutionBasics.hidden = false
|
|
||||||
|
|
||||||
this.domExecutionDetails.hidden = true
|
|
||||||
|
|
||||||
await window.terminal.reset()
|
|
||||||
window.terminal.fit()
|
|
||||||
}
|
|
||||||
|
|
||||||
show (actionButton) {
|
|
||||||
if (typeof actionButton !== 'undefined' && actionButton != null) {
|
|
||||||
this.domIcon.innerText = actionButton.domIcon.innerText
|
|
||||||
}
|
|
||||||
|
|
||||||
this.domBtnKill.disabled = false
|
|
||||||
this.domBtnKill.onclick = () => {
|
|
||||||
this.killAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
clearInterval(window.executionDialogTicker)
|
|
||||||
this.executionSeconds = 0
|
|
||||||
this.executionTick()
|
|
||||||
window.executionDialogTicker = setInterval(() => {
|
|
||||||
this.executionTick()
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
if (this.dlg.open) {
|
|
||||||
this.dlg.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dlg.showModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
rerunAction (actionId) {
|
|
||||||
const actionButton = document.getElementById('actionButton-' + actionId)
|
|
||||||
|
|
||||||
if (actionButton !== undefined) {
|
|
||||||
actionButton.btn.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dlg.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
killAction () {
|
|
||||||
const killActionArgs = {
|
|
||||||
executionTrackingId: this.executionTrackingId
|
|
||||||
}
|
|
||||||
|
|
||||||
window.fetch(window.restBaseUrl + 'KillAction', {
|
|
||||||
cors: 'cors',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(killActionArgs)
|
|
||||||
}).then((res) => {
|
|
||||||
return res.json() // This isn't used by anything. UI is updated by OnExecutionFinished like normal.
|
|
||||||
}).catch(err => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
executionTick () {
|
|
||||||
this.executionSeconds++
|
|
||||||
|
|
||||||
this.updateDuration(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
hideEverythingApartFromOutput () {
|
|
||||||
this.hideDetailsOnResult = true
|
|
||||||
this.domExecutionBasics.hidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchExecutionResult (executionTrackingId) {
|
|
||||||
this.executionTrackingId = executionTrackingId
|
|
||||||
|
|
||||||
const executionStatusArgs = {
|
|
||||||
executionTrackingId: this.executionTrackingId
|
|
||||||
}
|
|
||||||
|
|
||||||
window.fetch(window.restBaseUrl + 'ExecutionStatus', {
|
|
||||||
cors: 'cors',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(executionStatusArgs)
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json()
|
|
||||||
} else if (res.status === 404) {
|
|
||||||
throw new Error('Execution not found: ' + executionTrackingId)
|
|
||||||
} else {
|
|
||||||
throw new Error(res.statusText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).then((json) => {
|
|
||||||
this.renderExecutionResult(json)
|
|
||||||
}).catch(err => {
|
|
||||||
console.log(err)
|
|
||||||
this.renderError(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDuration (logEntry) {
|
|
||||||
if (logEntry == null) {
|
|
||||||
this.domDuration.innerHTML = this.executionSeconds + ' seconds'
|
|
||||||
} else if (!logEntry.executionStarted) {
|
|
||||||
this.domDuration.innerHTML = logEntry.datetimeStarted + ' (request time). Not executed.'
|
|
||||||
} else if (logEntry.executionStarted && !logEntry.executionFinished) {
|
|
||||||
this.domDuration.innerHTML = logEntry.datetimeStarted
|
|
||||||
} else {
|
|
||||||
let delta = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
delta = (new Date(logEntry.datetimeStarted) - new Date(logEntry.datetimeStarted)) / 1000
|
|
||||||
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to calculate delta', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.domDuration.innerHTML = logEntry.datetimeStarted + ' → ' + logEntry.datetimeFinished
|
|
||||||
|
|
||||||
if (delta !== '') {
|
|
||||||
this.domDuration.innerHTML += ' (' + delta + ')'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderExecutionResult (res) {
|
|
||||||
this.res = res
|
|
||||||
|
|
||||||
clearInterval(window.executionDialogTicker)
|
|
||||||
|
|
||||||
if ('type' in res && res.type === 'execution-dialog-output-html') {
|
|
||||||
this.domOutputHtml.hidden = false
|
|
||||||
this.domOutput.hidden = true
|
|
||||||
this.domOutputHtml.innerHTML = res.logEntry.output
|
|
||||||
this.domOutputHtml.hidden = false
|
|
||||||
this.hideDetailsonResult = true
|
|
||||||
} else {
|
|
||||||
this.domOutput.hidden = false
|
|
||||||
this.domOutputHtml.innerHTML = ''
|
|
||||||
this.domOutputHtml.hidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hideDetailsOnResult) {
|
|
||||||
this.domExecutionDetails.hidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.executionTrackingId = res.logEntry.executionTrackingId
|
|
||||||
|
|
||||||
this.domBtnRerun.disabled = !res.logEntry.executionFinished
|
|
||||||
this.domBtnRerun.onclick = () => { this.rerunAction(res.logEntry.actionId) }
|
|
||||||
|
|
||||||
this.domBtnKill.disabled = !res.logEntry.canKill
|
|
||||||
|
|
||||||
this.domStatus.update(res.logEntry)
|
|
||||||
|
|
||||||
this.domIcon.innerHTML = res.logEntry.actionIcon
|
|
||||||
this.domTitle.innerText = res.logEntry.actionTitle
|
|
||||||
this.domTitle.title = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId
|
|
||||||
|
|
||||||
this.updateDuration(res.logEntry)
|
|
||||||
|
|
||||||
await window.terminal.reset()
|
|
||||||
await window.terminal.write(res.logEntry.output, () => {
|
|
||||||
window.terminal.fit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderError (err) {
|
|
||||||
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
export class NavigationBar {
|
|
||||||
constructor () {
|
|
||||||
this.navbar = document.getElementsByTagName('nav')[0]
|
|
||||||
this.mainLinks = document.getElementById('navigation-links')
|
|
||||||
this.supplementalLinks = document.getElementById('supplemental-links')
|
|
||||||
}
|
|
||||||
|
|
||||||
createLink (title, url, isSupplemental) {
|
|
||||||
const parent = (isSupplemental) ? this.supplementalLinks : this.mainLinks
|
|
||||||
|
|
||||||
const existsAlready = Array.from(parent.querySelectorAll('li')).some(el => el.title === title)
|
|
||||||
|
|
||||||
if (existsAlready) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkA = document.createElement('a')
|
|
||||||
linkA.href = url
|
|
||||||
linkA.innerText = title
|
|
||||||
|
|
||||||
const navigationLi = document.createElement('li')
|
|
||||||
navigationLi.appendChild(linkA)
|
|
||||||
navigationLi.title = title
|
|
||||||
|
|
||||||
parent.appendChild(navigationLi)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshSectionPolicyLinks (policy) {
|
|
||||||
if (policy.showDiagnostics) {
|
|
||||||
this.createLink('Diagnostics', '/diagnostics', true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policy.showLogList) {
|
|
||||||
this.createLink('Logs', '/logs', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import {
|
|
||||||
refreshServerConnectionLabel
|
|
||||||
} from './marshaller.js'
|
|
||||||
|
|
||||||
window.ws = null
|
|
||||||
|
|
||||||
export function checkWebsocketConnection () {
|
|
||||||
if (window.ws === null || window.ws.readyState === 3) {
|
|
||||||
reconnectWebsocket()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reconnectWebsocket () {
|
|
||||||
window.websocketAvailable = false
|
|
||||||
|
|
||||||
const websocketConnectionUrl = new URL(window.location.toString())
|
|
||||||
websocketConnectionUrl.hash = ''
|
|
||||||
websocketConnectionUrl.pathname = '/websocket'
|
|
||||||
|
|
||||||
if (window.location.protocol === 'https:') {
|
|
||||||
websocketConnectionUrl.protocol = 'wss'
|
|
||||||
} else {
|
|
||||||
websocketConnectionUrl.protocol = 'ws'
|
|
||||||
}
|
|
||||||
|
|
||||||
window.websocketConnectionUrl = websocketConnectionUrl
|
|
||||||
|
|
||||||
const ws = window.ws = new WebSocket(websocketConnectionUrl.toString())
|
|
||||||
|
|
||||||
ws.addEventListener('open', websocketOnOpen)
|
|
||||||
ws.addEventListener('message', websocketOnMessage)
|
|
||||||
ws.addEventListener('error', websocketOnError)
|
|
||||||
ws.addEventListener('close', websocketOnClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
function websocketOnOpen (evt) {
|
|
||||||
window.websocketAvailable = true
|
|
||||||
|
|
||||||
window.ws.send('monitor')
|
|
||||||
|
|
||||||
refreshServerConnectionLabel()
|
|
||||||
|
|
||||||
window.refreshLoop()
|
|
||||||
}
|
|
||||||
|
|
||||||
function websocketOnMessage (msg) {
|
|
||||||
// FIXME check msg status is OK
|
|
||||||
const j = JSON.parse(msg.data)
|
|
||||||
j.type = j.type.replace('olivetin.api.v1.', '')
|
|
||||||
|
|
||||||
const e = new Event(j.type)
|
|
||||||
e.payload = j.payload
|
|
||||||
|
|
||||||
switch (j.type) {
|
|
||||||
case 'EventOutputChunk':
|
|
||||||
case 'EventConfigChanged':
|
|
||||||
case 'EventExecutionFinished':
|
|
||||||
case 'EventExecutionStarted':
|
|
||||||
case 'EventEntityChanged':
|
|
||||||
window.dispatchEvent(e)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + j.type, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function websocketOnError (err) {
|
|
||||||
window.websocketAvailable = false
|
|
||||||
window.refreshLoop()
|
|
||||||
|
|
||||||
console.log('Websocket error is: ', err)
|
|
||||||
|
|
||||||
window.showBigError('websocket-connection', 'connecting to the websocket', 'This often means the connection was closed, sometimes this can happen due to reverse proxy timeouts. Sometimes your web browser can provide helpful diagnostic information in the web developer console. The reason given by your browser is:' + err, true)
|
|
||||||
|
|
||||||
refreshServerConnectionLabel()
|
|
||||||
}
|
|
||||||
|
|
||||||
function websocketOnClose () {
|
|
||||||
window.websocketAvailable = false
|
|
||||||
|
|
||||||
refreshServerConnectionLabel()
|
|
||||||
}
|
|
||||||
|
|
@ -1,782 +0,0 @@
|
||||||
html, body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #dee3e7;
|
|
||||||
color: black;
|
|
||||||
font-family: sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
box-shadow: 0 0 6px 0 #444;
|
|
||||||
max-width: 600px;
|
|
||||||
min-width: 60%;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog[open] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.big {
|
|
||||||
max-width: 100vw;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
max-height: 100dvh;
|
|
||||||
border: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
font-family: sans-serif;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, 180px);
|
|
||||||
grid-auto-rows: 1fr;
|
|
||||||
grid-gap: 1em;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer span {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a {
|
|
||||||
display: block;
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
user-select: none;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
outline: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a:hover {
|
|
||||||
background-color: #efefef;
|
|
||||||
color: black;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a.selected {
|
|
||||||
background-color: #c4cdd4;
|
|
||||||
color: black;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar-toggler-button {
|
|
||||||
height: 2em;
|
|
||||||
width: 2em;
|
|
||||||
text-align: center;
|
|
||||||
display: inline-grid;
|
|
||||||
place-items: center;
|
|
||||||
margin-left: 1em;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userinfo {
|
|
||||||
padding-right: 1em;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userinfo svg, .userinfo span {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
left: -250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.topbar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
place-content: end;
|
|
||||||
border-radius: .5em;
|
|
||||||
padding-right: 1em;
|
|
||||||
padding-left: 1em;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.sidebar {
|
|
||||||
background-color: white;
|
|
||||||
position: fixed;
|
|
||||||
width: 180px;
|
|
||||||
height: 100dvh;
|
|
||||||
top: 0;
|
|
||||||
transition: left 0.5s ease, visibility 0.5s ease;
|
|
||||||
box-shadow: 0 0 10px 0 #444;
|
|
||||||
z-index: -1;
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.sidebar.shown {
|
|
||||||
visibility: visible;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar #navigation-links {
|
|
||||||
padding-top: 4em;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#supplemental-links {
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
display: inline;
|
|
||||||
font-size: small;
|
|
||||||
padding-left: .5em;
|
|
||||||
flex-grow: 1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog h1 {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.topbar ul {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.topbar ul li {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.topbar ul li a {
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.sidebar ul li {
|
|
||||||
list-style: none;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
background-color: white;
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border-top: 1px solid #efefef;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
th:first-child {
|
|
||||||
width: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover td {
|
|
||||||
background-color: beige;
|
|
||||||
}
|
|
||||||
|
|
||||||
legend {
|
|
||||||
font-family: sans-serif;
|
|
||||||
padding-top: 1em;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon {
|
|
||||||
display: block;
|
|
||||||
font-size: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon img {
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-header span.icon,
|
|
||||||
tr.log-row span.icon {
|
|
||||||
display: inline-block;
|
|
||||||
padding-right: 0.2em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: salmon;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title.temporary-status-message {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1em;
|
|
||||||
margin-top: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.entity h2 {
|
|
||||||
grid-column: 1 / span all;
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
|
||||||
display: inline-block;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General Buttons */
|
|
||||||
|
|
||||||
button {
|
|
||||||
transition: none;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: .7em;
|
|
||||||
box-shadow: none;
|
|
||||||
font-size: 1em;
|
|
||||||
padding: 0.6em 1em;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
font-family: sans-serif;
|
|
||||||
color: black;
|
|
||||||
text-align: center;
|
|
||||||
background-color: white;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button button {
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: normal;
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 1em;
|
|
||||||
box-shadow: 0 0 6px 0 #aaa;
|
|
||||||
transition: background-color 1s ease, color .3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
execution-button button {
|
|
||||||
margin-top: 0.2em;
|
|
||||||
margin-bottom: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #ececec;
|
|
||||||
color: black;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
color: #3c3c3c;
|
|
||||||
background-color: #cecece;
|
|
||||||
cursor: not-allowed;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[name="cancel"]:hover {
|
|
||||||
background-color: salmon;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[name="start"]:hover {
|
|
||||||
background-color: #aceaac;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button button:hover {
|
|
||||||
box-shadow: 0 0 10px 0 #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.title {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button details {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button details[open] {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button details summary div {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button details summary div span:first-child {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button-footer {
|
|
||||||
text-align: left;
|
|
||||||
font-size: smaller;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
execution-button {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: .2em;
|
|
||||||
margin-left: .2em;
|
|
||||||
margin-top: .2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-status {
|
|
||||||
padding: .4em;
|
|
||||||
border-radius: .4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-failed {
|
|
||||||
background-color: #e78284;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-success {
|
|
||||||
background-color: #a6d189;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-nonzero-exit {
|
|
||||||
background-color: #ef9f76;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-timeout {
|
|
||||||
background-color: #99d1db;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-blocked {
|
|
||||||
background-color: #ca9ee6;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.logo {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 1em;
|
|
||||||
padding-top: 3.5em;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.wrapper {
|
|
||||||
background-color: white;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
text-align: right;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
z-index: 3;
|
|
||||||
align-items: center;
|
|
||||||
padding-bottom: .2em;
|
|
||||||
padding-top: 1em;
|
|
||||||
position: fixed;
|
|
||||||
background-color: #dee3e7;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
font-family: sans-serif;
|
|
||||||
padding: 0.6em;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: .4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:invalid {
|
|
||||||
outline: 2px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
justify-self: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .wrapper span.icon {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.arguments {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: max-content auto auto; /* We don't want the label or the description to wrap, and the input to take up the rest of the space */
|
|
||||||
grid-template-rows: auto;
|
|
||||||
grid-gap: 1em;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
p.argument-wrapper {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: end;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.invalid {
|
|
||||||
background-color: salmon;
|
|
||||||
}
|
|
||||||
|
|
||||||
#available-version {
|
|
||||||
background-color: #aceaac;
|
|
||||||
padding: 0.2em;
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.tag {
|
|
||||||
border-radius: 0.4em;
|
|
||||||
margin-top: .2em;
|
|
||||||
margin-right: .2em;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: lightgray;
|
|
||||||
padding: .4em;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.annotation {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: .2em;
|
|
||||||
margin-right: .2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.annotation-key {
|
|
||||||
padding: 0.4em;
|
|
||||||
border-radius: 0.4em 0 0 .4em;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: lightgray;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.annotation-value {
|
|
||||||
padding: 0.4em;
|
|
||||||
border-radius: 0 .4em .4em 0;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: lightgray;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-shadow {
|
|
||||||
box-shadow: 0 0 5px 0 #444;
|
|
||||||
background-color: #fff;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-radius {
|
|
||||||
border-radius: .7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-shadow p {
|
|
||||||
padding: .6em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.toolbar {
|
|
||||||
padding: .4em;
|
|
||||||
text-align: left;
|
|
||||||
background-color: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.display {
|
|
||||||
border: 1px solid #666;
|
|
||||||
box-shadow: 0 0 6px 0 #aaa;
|
|
||||||
border-radius: .7em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
place-content: center center;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
#execution-dialog-xterm {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.padded-content {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.padded-content-sides {
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ta-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.input-with-icons {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.input-with-icons input {
|
|
||||||
border: 0;
|
|
||||||
padding: .6em;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.input-with-icons svg {
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.input-with-icons button {
|
|
||||||
border: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
transition: color .3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.input-with-icons button:disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-login hr {
|
|
||||||
border: 0;
|
|
||||||
border-top: 1px dashed #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-login button {
|
|
||||||
width: 100%;
|
|
||||||
grid-column: 1 / span 3;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-login button:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-login ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-login form {
|
|
||||||
width: auto;
|
|
||||||
place-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth2-icon {
|
|
||||||
font-size: 1.8em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (width <= 600px) {
|
|
||||||
fieldset {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: .6em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.argument-wrapper {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.arguments {
|
|
||||||
grid-template-columns: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
width: 100vw;
|
|
||||||
max-width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body, header {
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-shadow {
|
|
||||||
box-shadow: 0 0 6px 0 #625c5c;
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
background-color: #222;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.wrapper {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.input-with-icons {
|
|
||||||
border: 1px solid #666;
|
|
||||||
background-color: #383838;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: #383838;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: 1px solid #666;
|
|
||||||
background-color: #222;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
action-button button {
|
|
||||||
box-shadow: 0 0 6px 0 #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: 2px solid #72B7F4;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #444;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
footer {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
a, a:visited {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus {
|
|
||||||
background-color: #72B7F4;
|
|
||||||
color: black;
|
|
||||||
outline: 1px solid #72B7F4;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.sidebar {
|
|
||||||
background-color: #111;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.sidebar ul li {
|
|
||||||
border-bottom: 1px solid #3e3e3e;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a:hover {
|
|
||||||
background-color: #1b5988;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a.selected {
|
|
||||||
background-color: #0c3351;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
table,
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-top: 1px solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
tr {
|
|
||||||
background-color: #222;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover td {
|
|
||||||
background-color: #1b5988;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.toolbar {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.display {
|
|
||||||
box-shadow: 0 0 6px 0 #444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||