chore: calendar fixes
This commit is contained in:
commit
a36e69b8f8
|
|
@ -50,7 +50,7 @@ actions:
|
||||||
popupOnStart: execution-button
|
popupOnStart: execution-button
|
||||||
maxRate:
|
maxRate:
|
||||||
- limit: 3
|
- limit: 3
|
||||||
duration: 5m
|
duration: 1m
|
||||||
|
|
||||||
# You are not limited to operating system commands, and of course you can run
|
# You are not limited to operating system commands, and of course you can run
|
||||||
# your own scripts. Here `maxConcurrent` stops the script running multiple
|
# your own scripts. Here `maxConcurrent` stops the script running multiple
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { buttonResults } from '../resources/vue/stores/buttonResults.js'
|
import { buttonResults } from '../resources/vue/stores/buttonResults.js'
|
||||||
|
import { rateLimits } from '../resources/vue/stores/rateLimits.js'
|
||||||
|
|
||||||
export function initWebsocket () {
|
export function initWebsocket () {
|
||||||
window.addEventListener('EventOutputChunk', onOutputChunk)
|
window.addEventListener('EventOutputChunk', onOutputChunk)
|
||||||
|
window.addEventListener('EventExecutionStarted', onExecutionChanged)
|
||||||
|
window.addEventListener('EventExecutionFinished', onExecutionChanged)
|
||||||
|
|
||||||
reconnectWebsocket()
|
reconnectWebsocket()
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +43,6 @@ function handleEvent (msg) {
|
||||||
break
|
break
|
||||||
case 'EventExecutionFinished':
|
case 'EventExecutionFinished':
|
||||||
case 'EventExecutionStarted':
|
case 'EventExecutionStarted':
|
||||||
buttonResults[msg.event.value.logEntry.executionTrackingId] = msg.event.value.logEntry
|
|
||||||
window.dispatchEvent(j)
|
window.dispatchEvent(j)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
|
@ -59,3 +61,19 @@ function onOutputChunk (evt) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onExecutionChanged (evt) {
|
||||||
|
buttonResults[evt.payload.logEntry.executionTrackingId] = evt.payload.logEntry
|
||||||
|
|
||||||
|
const logEntry = evt.payload.logEntry
|
||||||
|
|
||||||
|
// Update rate limit store from logEntry if rate limit expiry datetime is provided
|
||||||
|
if (logEntry && logEntry.datetimeRateLimitExpires && logEntry.bindingId) {
|
||||||
|
// Parse datetime string "2006-01-02 15:04:05" and convert to Unix timestamp
|
||||||
|
const date = new Date(logEntry.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||||
|
rateLimits[logEntry.bindingId] = date.getTime() / 1000
|
||||||
|
} else if (logEntry && logEntry.bindingId) {
|
||||||
|
// Clear rate limit if not set
|
||||||
|
rateLimits[logEntry.bindingId] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,11 +17,10 @@
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.12.5",
|
"picocrank": "^1.13.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vue": "^3.5.26",
|
|
||||||
"vue-i18n": "^11.2.8",
|
"vue-i18n": "^11.2.8",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
|
|
@ -4608,9 +4607,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picocrank": {
|
"node_modules/picocrank": {
|
||||||
"version": "1.12.5",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.13.0.tgz",
|
||||||
"integrity": "sha512-z0EP/I56cFGzvXV4EAEpkczYDYkdHGtRfHQA+k7rbrBEHMO1fi7qW8VbDj7/2eqeG6IbNqWRIG1IexRQWZj7bQ==",
|
"integrity": "sha512-kfUU2KVZnHyZ3/s+vM2tw25G+rK//eMjhF7cuXNQm0Ar/o5YvPiLBx8uxCrNNNvhGqb+jQK1Jbn41KO6rAR8Lw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hugeicons/core-free-icons": "^3.1.1",
|
"@hugeicons/core-free-icons": "^3.1.1",
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,11 @@
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.12.5",
|
"picocrank": "^1.13.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-i18n": "^11.2.8",
|
"vue-i18n": "^11.2.8",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,13 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & {
|
||||||
* @generated from field: int32 timeout = 8;
|
* @generated from field: int32 timeout = 8;
|
||||||
*/
|
*/
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
|
*
|
||||||
|
* @generated from field: string datetime_rate_limit_expires = 9;
|
||||||
|
*/
|
||||||
|
datetimeRateLimitExpires: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -99,6 +106,11 @@ export declare type ActionArgument = Message<"olivetin.api.v1.ActionArgument"> &
|
||||||
* @generated from field: map<string, string> suggestions = 7;
|
* @generated from field: map<string, string> suggestions = 7;
|
||||||
*/
|
*/
|
||||||
suggestions: { [key: string]: string };
|
suggestions: { [key: string]: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string suggestions_browser_key = 8;
|
||||||
|
*/
|
||||||
|
suggestionsBrowserKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -476,6 +488,13 @@ export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> &
|
||||||
* @generated from field: int64 start_offset = 1;
|
* @generated from field: int64 start_offset = 1;
|
||||||
*/
|
*/
|
||||||
startOffset: bigint;
|
startOffset: bigint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional date filter in YYYY-MM-DD format
|
||||||
|
*
|
||||||
|
* @generated from field: string date_filter = 2;
|
||||||
|
*/
|
||||||
|
dateFilter: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -543,11 +562,6 @@ export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & {
|
||||||
*/
|
*/
|
||||||
datetimeFinished: string;
|
datetimeFinished: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* @generated from field: string action_id = 13;
|
|
||||||
*/
|
|
||||||
actionId: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from field: bool execution_started = 14;
|
* @generated from field: bool execution_started = 14;
|
||||||
*/
|
*/
|
||||||
|
|
@ -572,6 +586,20 @@ export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & {
|
||||||
* @generated from field: bool can_kill = 18;
|
* @generated from field: bool can_kill = 18;
|
||||||
*/
|
*/
|
||||||
canKill: boolean;
|
canKill: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
|
*
|
||||||
|
* @generated from field: string datetime_rate_limit_expires = 19;
|
||||||
|
*/
|
||||||
|
datetimeRateLimitExpires: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binding ID for matching rate limits to action buttons
|
||||||
|
*
|
||||||
|
* @generated from field: string binding_id = 20;
|
||||||
|
*/
|
||||||
|
bindingId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -904,9 +932,9 @@ export declare type DumpVarsResponse = Message<"olivetin.api.v1.DumpVarsResponse
|
||||||
export declare const DumpVarsResponseSchema: GenMessage<DumpVarsResponse>;
|
export declare const DumpVarsResponseSchema: GenMessage<DumpVarsResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message olivetin.api.v1.ActionEntityPair
|
* @generated from message olivetin.api.v1.DebugBinding
|
||||||
*/
|
*/
|
||||||
export declare type ActionEntityPair = Message<"olivetin.api.v1.ActionEntityPair"> & {
|
export declare type DebugBinding = Message<"olivetin.api.v1.DebugBinding"> & {
|
||||||
/**
|
/**
|
||||||
* @generated from field: string action_title = 1;
|
* @generated from field: string action_title = 1;
|
||||||
*/
|
*/
|
||||||
|
|
@ -919,10 +947,10 @@ export declare type ActionEntityPair = Message<"olivetin.api.v1.ActionEntityPair
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the message olivetin.api.v1.ActionEntityPair.
|
* Describes the message olivetin.api.v1.DebugBinding.
|
||||||
* Use `create(ActionEntityPairSchema)` to create a new message.
|
* Use `create(DebugBindingSchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export declare const ActionEntityPairSchema: GenMessage<ActionEntityPair>;
|
export declare const DebugBindingSchema: GenMessage<DebugBinding>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message olivetin.api.v1.DumpPublicIdActionMapRequest
|
* @generated from message olivetin.api.v1.DumpPublicIdActionMapRequest
|
||||||
|
|
@ -946,9 +974,9 @@ export declare type DumpPublicIdActionMapResponse = Message<"olivetin.api.v1.Dum
|
||||||
alert: string;
|
alert: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from field: map<string, olivetin.api.v1.ActionEntityPair> contents = 2;
|
* @generated from field: map<string, olivetin.api.v1.DebugBinding> contents = 2;
|
||||||
*/
|
*/
|
||||||
contents: { [key: string]: ActionEntityPair };
|
contents: { [key: string]: DebugBinding };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :id="`actionButton-${actionId}`" role="none" class="action-button">
|
<div :id="`actionButton-${bindingId}`" role="none" class="action-button">
|
||||||
<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
|
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
||||||
:class="buttonClasses" @click="handleClick">
|
:class="combinedClasses" @click="handleClick">
|
||||||
|
|
||||||
<div class="navigate-on-start-container">
|
<div class="navigate-on-start-container">
|
||||||
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
||||||
|
|
@ -18,18 +18,19 @@
|
||||||
<span class="icon" v-html="unicodeIcon"></span>
|
<span class="icon" v-html="unicodeIcon"></span>
|
||||||
<span class="title" aria-live="polite">{{ displayTitle }}
|
<span class="title" aria-live="polite">{{ displayTitle }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="rateLimitMessage" class="rate-limit-message">{{ rateLimitMessage }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ArgumentForm from './views/ArgumentForm.vue'
|
|
||||||
import { buttonResults } from './stores/buttonResults'
|
import { buttonResults } from './stores/buttonResults'
|
||||||
|
import { rateLimits } from './stores/rateLimits'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons'
|
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons'
|
||||||
|
|
||||||
import { ref, watch, onMounted, inject } from 'vue'
|
import { ref, watch, onMounted, onUnmounted, inject, computed } from 'vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const navigateOnStart = ref('')
|
const navigateOnStart = ref('')
|
||||||
|
|
@ -38,10 +39,15 @@ const props = defineProps({
|
||||||
actionData: {
|
actionData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
cssClass: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionId = ref('')
|
const bindingId = ref('')
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const canExec = ref(true)
|
const canExec = ref(true)
|
||||||
const popupOnStart = ref('')
|
const popupOnStart = ref('')
|
||||||
|
|
@ -54,9 +60,24 @@ const displayTitle = ref('')
|
||||||
const isDisabled = ref(false)
|
const isDisabled = ref(false)
|
||||||
const showArgumentForm = ref(false)
|
const showArgumentForm = ref(false)
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const rateLimitExpires = ref(0)
|
||||||
|
const isRateLimited = ref(false)
|
||||||
|
const rateLimitMessage = ref('')
|
||||||
|
let rateLimitInterval = null
|
||||||
|
|
||||||
// Animation classes
|
// Animation classes
|
||||||
const buttonClasses = ref([])
|
const buttonClasses = ref([])
|
||||||
|
|
||||||
|
// Combined classes including custom cssClass
|
||||||
|
const combinedClasses = computed(() => {
|
||||||
|
const classes = [...buttonClasses.value]
|
||||||
|
if (props.cssClass) {
|
||||||
|
classes.push(props.cssClass)
|
||||||
|
}
|
||||||
|
return classes
|
||||||
|
})
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
const updateIterationTimestamp = ref(0)
|
const updateIterationTimestamp = ref(0)
|
||||||
|
|
||||||
|
|
@ -75,7 +96,7 @@ function constructFromJson(json) {
|
||||||
|
|
||||||
updateFromJson(json)
|
updateFromJson(json)
|
||||||
|
|
||||||
actionId.value = json.bindingId
|
bindingId.value = json.bindingId
|
||||||
title.value = json.title
|
title.value = json.title
|
||||||
canExec.value = json.canExec
|
canExec.value = json.canExec
|
||||||
popupOnStart.value = json.popupOnStart
|
popupOnStart.value = json.popupOnStart
|
||||||
|
|
@ -89,6 +110,19 @@ function constructFromJson(json) {
|
||||||
isDisabled.value = !json.canExec
|
isDisabled.value = !json.canExec
|
||||||
displayTitle.value = title.value
|
displayTitle.value = title.value
|
||||||
unicodeIcon.value = getUnicodeIcon(json.icon)
|
unicodeIcon.value = getUnicodeIcon(json.icon)
|
||||||
|
|
||||||
|
// Initialize rate limit from action data (parse datetime string)
|
||||||
|
if (json.datetimeRateLimitExpires) {
|
||||||
|
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||||
|
rateLimitExpires.value = date.getTime() / 1000
|
||||||
|
} else {
|
||||||
|
rateLimitExpires.value = 0
|
||||||
|
}
|
||||||
|
// Also initialize the store so the watch picks it up
|
||||||
|
if (bindingId.value) {
|
||||||
|
rateLimits[bindingId.value] = rateLimitExpires.value
|
||||||
|
}
|
||||||
|
updateRateLimitStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromJson(json) {
|
function updateFromJson(json) {
|
||||||
|
|
@ -96,6 +130,55 @@ function updateFromJson(json) {
|
||||||
// title - as the callback URL relies on it
|
// title - as the callback URL relies on it
|
||||||
|
|
||||||
unicodeIcon.value = getUnicodeIcon(json.icon)
|
unicodeIcon.value = getUnicodeIcon(json.icon)
|
||||||
|
|
||||||
|
// Update rate limiting if changed (parse datetime string)
|
||||||
|
if (json.datetimeRateLimitExpires) {
|
||||||
|
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
|
||||||
|
rateLimitExpires.value = date.getTime() / 1000
|
||||||
|
updateRateLimitStatus()
|
||||||
|
} else if (json.datetimeRateLimitExpires === '') {
|
||||||
|
// Explicitly clear if empty string
|
||||||
|
rateLimitExpires.value = 0
|
||||||
|
updateRateLimitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRateLimitStatus() {
|
||||||
|
if (rateLimitExpires.value === 0) {
|
||||||
|
isRateLimited.value = false
|
||||||
|
rateLimitMessage.value = ''
|
||||||
|
if (rateLimitInterval) {
|
||||||
|
clearInterval(rateLimitInterval)
|
||||||
|
rateLimitInterval = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
const expires = rateLimitExpires.value
|
||||||
|
|
||||||
|
if (now >= expires) {
|
||||||
|
// Rate limit has expired
|
||||||
|
isRateLimited.value = false
|
||||||
|
rateLimitMessage.value = ''
|
||||||
|
rateLimitExpires.value = 0
|
||||||
|
if (rateLimitInterval) {
|
||||||
|
clearInterval(rateLimitInterval)
|
||||||
|
rateLimitInterval = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Still rate limited
|
||||||
|
isRateLimited.value = true
|
||||||
|
const secondsRemaining = expires - now
|
||||||
|
rateLimitMessage.value = `Rate limited, available in ${secondsRemaining} second${secondsRemaining !== 1 ? 's' : ''}`
|
||||||
|
|
||||||
|
// Set up interval to update every second
|
||||||
|
if (!rateLimitInterval) {
|
||||||
|
rateLimitInterval = setInterval(() => {
|
||||||
|
updateRateLimitStatus()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClick() {
|
async function handleClick() {
|
||||||
|
|
@ -199,6 +282,30 @@ function onExecStatusChanged() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
constructFromJson(props.actionData)
|
constructFromJson(props.actionData)
|
||||||
|
|
||||||
|
// Watch the central rate limit store for updates to this button's bindingId
|
||||||
|
// Watch the entire rateLimits object to ensure reactivity with dynamic keys
|
||||||
|
watch(
|
||||||
|
rateLimits,
|
||||||
|
() => {
|
||||||
|
const id = bindingId.value
|
||||||
|
if (id && rateLimits[id] !== undefined) {
|
||||||
|
const newExpires = rateLimits[id]
|
||||||
|
if (newExpires !== rateLimitExpires.value) {
|
||||||
|
rateLimitExpires.value = newExpires
|
||||||
|
updateRateLimitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (rateLimitInterval) {
|
||||||
|
clearInterval(rateLimitInterval)
|
||||||
|
rateLimitInterval = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -256,6 +363,13 @@ watch(
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-button button .rate-limit-message {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: #856404;
|
||||||
|
padding: 0.2em;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* Animation classes */
|
/* Animation classes */
|
||||||
.action-button button.action-timeout {
|
.action-button button.action-timeout {
|
||||||
background: #fff3cd;
|
background: #fff3cd;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="showNavigation">
|
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="sidebarEnabled" :topBarEnabled="topbarEnabled" :navigation="navigation">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
||||||
<p>{{ bannerMessage }}</p>
|
<p>{{ bannerMessage }}</p>
|
||||||
|
|
@ -19,8 +19,8 @@
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<div id="layout">
|
<div id="layout">
|
||||||
<Navigation ref="navigation" v-if="showNavigation">
|
<Navigation ref="navigation">
|
||||||
<Sidebar ref="sidebar" id = "mainnav" v-if="showNavigation" />
|
<Sidebar ref="sidebar" id = "mainnav" v-if="sidebarEnabled && showNavigation"/>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
|
|
||||||
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
||||||
|
|
@ -108,6 +108,7 @@ const showNavigation = ref(true)
|
||||||
const showLogs = ref(true)
|
const showLogs = ref(true)
|
||||||
const showDiagnostics = ref(true)
|
const showDiagnostics = ref(true)
|
||||||
const showLoginLink = ref(true)
|
const showLoginLink = ref(true)
|
||||||
|
const sectionNavigationStyle = ref('sidebar')
|
||||||
|
|
||||||
const languageDialog = ref(null)
|
const languageDialog = ref(null)
|
||||||
const browserLanguages = ref([])
|
const browserLanguages = ref([])
|
||||||
|
|
@ -135,6 +136,15 @@ const currentLanguageName = computed(() => {
|
||||||
return availableLanguages[languagePreference.value] || languagePreference.value
|
return availableLanguages[languagePreference.value] || languagePreference.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Computed properties for navigation style
|
||||||
|
const topbarEnabled = computed(() => {
|
||||||
|
return sectionNavigationStyle.value === 'topbar'
|
||||||
|
})
|
||||||
|
|
||||||
|
const sidebarEnabled = computed(() => {
|
||||||
|
return sectionNavigationStyle.value !== 'topbar' && showNavigation.value
|
||||||
|
})
|
||||||
|
|
||||||
function normalizeBrowserLanguage() {
|
function normalizeBrowserLanguage() {
|
||||||
const available = Object.keys(combinedTranslations.messages || {})
|
const available = Object.keys(combinedTranslations.messages || {})
|
||||||
|
|
||||||
|
|
@ -180,6 +190,7 @@ function updateHeaderFromInit() {
|
||||||
showNavigation.value = window.initResponse.showNavigation
|
showNavigation.value = window.initResponse.showNavigation
|
||||||
showLogs.value = window.initResponse.showLogList
|
showLogs.value = window.initResponse.showLogList
|
||||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||||
|
sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
|
||||||
|
|
||||||
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||||
showLoginLink.value = false
|
showLoginLink.value = false
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
<span v-else>{{ component.title }}</span>
|
<span v-else>{{ component.title }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset :class="component.cssClass">
|
||||||
<template v-for="subcomponent in component.contents">
|
<template v-for="subcomponent in component.contents">
|
||||||
<DashboardComponent :component="subcomponent" />
|
<DashboardComponent :component="subcomponent" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :key="component.title" />
|
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :cssClass="component.cssClass" :key="component.title" />
|
||||||
|
|
||||||
<DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
|
<DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="navigateToDirectory">
|
<button @click="navigateToDirectory" :class="component.cssClass">
|
||||||
<span class="icon" v-html="unicodeIcon"></span>
|
<span class="icon" v-html="unicodeIcon"></span>
|
||||||
<span class="title">{{ component.title }}</span>
|
<span class="title">{{ component.title }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="display">
|
<div class="display" :class="component.cssClass">
|
||||||
<div v-html="component.title" />
|
<div v-html="component.title" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mre-container">
|
<div class="mre-container" :class="component.cssClass">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="executionTrackingId"
|
v-if="executionTrackingId"
|
||||||
:to="`/logs/${executionTrackingId}`"
|
:to="`/logs/${executionTrackingId}`"
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
|
import { buttonResults } from '../stores/buttonResults'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
|
|
@ -23,7 +24,20 @@ const props = defineProps({
|
||||||
|
|
||||||
const output = ref('Waiting...')
|
const output = ref('Waiting...')
|
||||||
const executionTrackingId = ref(null)
|
const executionTrackingId = ref(null)
|
||||||
let eventListener = null
|
let unwatchButtonResults = null
|
||||||
|
|
||||||
|
function updateFromLogEntry(logEntry) {
|
||||||
|
if (logEntry) {
|
||||||
|
if (logEntry.output !== undefined) {
|
||||||
|
output.value = logEntry.output
|
||||||
|
} else {
|
||||||
|
output.value = 'No output available'
|
||||||
|
}
|
||||||
|
if (logEntry.executionTrackingId) {
|
||||||
|
executionTrackingId.value = logEntry.executionTrackingId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchMostRecentExecution() {
|
async function fetchMostRecentExecution() {
|
||||||
if (!props.component.title) {
|
if (!props.component.title) {
|
||||||
|
|
@ -46,14 +60,7 @@ async function fetchMostRecentExecution() {
|
||||||
const result = await window.client.executionStatus(executionStatusArgs)
|
const result = await window.client.executionStatus(executionStatusArgs)
|
||||||
|
|
||||||
if (result.logEntry) {
|
if (result.logEntry) {
|
||||||
if (result.logEntry.output !== undefined) {
|
updateFromLogEntry(result.logEntry)
|
||||||
output.value = result.logEntry.output
|
|
||||||
} else {
|
|
||||||
output.value = 'No output available'
|
|
||||||
}
|
|
||||||
if (result.logEntry.executionTrackingId) {
|
|
||||||
executionTrackingId.value = result.logEntry.executionTrackingId
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
output.value = 'No output available'
|
output.value = 'No output available'
|
||||||
executionTrackingId.value = null
|
executionTrackingId.value = null
|
||||||
|
|
@ -70,32 +77,39 @@ async function fetchMostRecentExecution() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecutionFinished(event) {
|
|
||||||
// The dashboard component "title" field is used for lots of things
|
|
||||||
// and in this context for MreOutput it's just to refer to an actionId.
|
|
||||||
//
|
|
||||||
// So this is not a typo.
|
|
||||||
const logEntry = event.payload.logEntry
|
|
||||||
if (logEntry && logEntry.actionId === props.component.title) {
|
|
||||||
if (logEntry.output !== undefined) {
|
|
||||||
output.value = logEntry.output
|
|
||||||
}
|
|
||||||
if (logEntry.executionTrackingId) {
|
|
||||||
executionTrackingId.value = logEntry.executionTrackingId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMostRecentExecution()
|
fetchMostRecentExecution()
|
||||||
|
|
||||||
eventListener = (event) => handleExecutionFinished(event)
|
unwatchButtonResults = watch(
|
||||||
window.addEventListener('EventExecutionFinished', eventListener)
|
buttonResults,
|
||||||
|
() => {
|
||||||
|
// Find the most recent finished execution for this bindingId
|
||||||
|
const bindingId = props.component.title
|
||||||
|
let mostRecent = null
|
||||||
|
let mostRecentTime = null
|
||||||
|
|
||||||
|
for (const trackingId in buttonResults) {
|
||||||
|
const logEntry = buttonResults[trackingId]
|
||||||
|
if (logEntry && logEntry.bindingId === bindingId && logEntry.executionFinished) {
|
||||||
|
const finishedTime = new Date(logEntry.datetimeFinished)
|
||||||
|
if (!mostRecent || finishedTime > mostRecentTime) {
|
||||||
|
mostRecent = logEntry
|
||||||
|
mostRecentTime = finishedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostRecent) {
|
||||||
|
updateFromLogEntry(mostRecent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (eventListener) {
|
if (unwatchButtonResults) {
|
||||||
window.removeEventListener('EventExecutionFinished', eventListener)
|
unwatchButtonResults()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
// Store rate limit expiry times by bindingId
|
||||||
|
// This allows all ActionButton components to reactively update when rate limits change
|
||||||
|
export const rateLimits = reactive({})
|
||||||
|
|
@ -12,10 +12,13 @@
|
||||||
{{ formatLabel(arg.title) }}
|
{{ formatLabel(arg.title) }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<datalist v-if="arg.suggestions && Object.keys(arg.suggestions).length > 0" :id="`${arg.name}-choices`">
|
<datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="`${arg.name}-choices`">
|
||||||
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
|
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
|
||||||
{{ suggestion }}
|
{{ suggestion }}
|
||||||
</option>
|
</option>
|
||||||
|
<option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
|
||||||
|
{{ suggestion }}
|
||||||
|
</option>
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
|
<select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
|
||||||
|
|
@ -28,7 +31,7 @@
|
||||||
<component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name"
|
<component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name"
|
||||||
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
|
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
|
||||||
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
|
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
|
||||||
:list="arg.suggestions ? `${arg.name}-choices` : undefined"
|
:list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? `${arg.name}-choices` : undefined"
|
||||||
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
|
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
|
||||||
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
||||||
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
|
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
|
||||||
|
|
@ -313,6 +316,60 @@ function getUniqueId() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBrowserSuggestions(arg) {
|
||||||
|
if (!arg.suggestionsBrowserKey) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(`olivetin-suggestions-${arg.suggestionsBrowserKey}`)
|
||||||
|
if (stored) {
|
||||||
|
const suggestions = JSON.parse(stored)
|
||||||
|
return Array.isArray(suggestions) ? suggestions : []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load browser suggestions:', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveBrowserSuggestions() {
|
||||||
|
for (const arg of actionArguments.value) {
|
||||||
|
if (arg.suggestionsBrowserKey) {
|
||||||
|
const value = argValues.value[arg.name]
|
||||||
|
|
||||||
|
// Only save non-empty values for non-checkbox/confirmation/password types
|
||||||
|
if (value && value !== '' && arg.type !== 'checkbox' && arg.type !== 'confirmation' && arg.type !== 'password') {
|
||||||
|
try {
|
||||||
|
const key = `olivetin-suggestions-${arg.suggestionsBrowserKey}`
|
||||||
|
const stored = localStorage.getItem(key)
|
||||||
|
let suggestions = []
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
suggestions = JSON.parse(stored)
|
||||||
|
if (!Array.isArray(suggestions)) {
|
||||||
|
suggestions = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add value if not already present
|
||||||
|
if (!suggestions.includes(value)) {
|
||||||
|
suggestions.unshift(value) // Add to beginning
|
||||||
|
// Keep only the most recent 50 suggestions
|
||||||
|
if (suggestions.length > 50) {
|
||||||
|
suggestions = suggestions.slice(0, 50)
|
||||||
|
}
|
||||||
|
localStorage.setItem(key, JSON.stringify(suggestions))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to save browser suggestions:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function startAction(actionArgs) {
|
async function startAction(actionArgs) {
|
||||||
const startActionArgs = {
|
const startActionArgs = {
|
||||||
bindingId: props.bindingId,
|
bindingId: props.bindingId,
|
||||||
|
|
@ -356,6 +413,9 @@ async function handleSubmit(event) {
|
||||||
const argvs = getArgumentValues()
|
const argvs = getArgumentValues()
|
||||||
console.log('argument form has elements that passed validation')
|
console.log('argument form has elements that passed validation')
|
||||||
|
|
||||||
|
// Save values to localStorage for arguments with suggestionsBrowserKey
|
||||||
|
saveBrowserSuggestions()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await startAction(argvs)
|
const response = await startAction(argvs)
|
||||||
router.push(`/logs/${response.executionTrackingId}`)
|
router.push(`/logs/${response.executionTrackingId}`)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
logLevel: debug
|
logLevel: debug
|
||||||
|
|
||||||
|
insecureAllowDumpActionMap: true
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- title: Check status
|
- title: Check status
|
||||||
id: status_command
|
id: status_command
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,7 @@ describe('config: stdout-most-recent-execution', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('stdout-most-recent-execution updates after action execution', async function () {
|
it('stdout-most-recent-execution updates after action execution', async function () {
|
||||||
this.timeout(30000) // Increase timeout for this test
|
this.timeout(45000)
|
||||||
|
|
||||||
await getRootAndWait()
|
await getRootAndWait()
|
||||||
|
|
||||||
// Wait for the mre-output element
|
// Wait for the mre-output element
|
||||||
|
|
@ -105,7 +104,7 @@ describe('config: stdout-most-recent-execution', function () {
|
||||||
await statusButton.click()
|
await statusButton.click()
|
||||||
|
|
||||||
// Wait a moment for the action to start
|
// Wait a moment for the action to start
|
||||||
await webdriver.sleep(500)
|
await webdriver.sleep(2000)
|
||||||
|
|
||||||
// Wait for the output to update (the component listens to EventExecutionFinished events)
|
// Wait for the output to update (the component listens to EventExecutionFinished events)
|
||||||
// We'll wait for the output to change from the initial state
|
// We'll wait for the output to change from the initial state
|
||||||
|
|
@ -138,4 +137,4 @@ describe('config: stdout-most-recent-execution', function () {
|
||||||
expect(updatedText).to.not.include('No execution found')
|
expect(updatedText).to.not.include('No execution found')
|
||||||
expect(updatedText.trim().length).to.be.greaterThan(0)
|
expect(updatedText.trim().length).to.be.greaterThan(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
actions:
|
||||||
|
- title: Test suggestionsBrowserKey
|
||||||
|
shell: "echo 'Input value: {{ testInput }}, Second input: {{ testInput2 }}'"
|
||||||
|
icon: ping
|
||||||
|
arguments:
|
||||||
|
- name: testInput
|
||||||
|
title: Test Input
|
||||||
|
description: "This input uses suggestionsBrowserKey"
|
||||||
|
suggestionsBrowserKey: test-suggestions-key
|
||||||
|
- name: testInput2
|
||||||
|
title: Test Input 2
|
||||||
|
description: "This input shares the same suggestionsBrowserKey"
|
||||||
|
suggestionsBrowserKey: test-suggestions-key
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
import { describe, it, before, after } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By, Condition } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
getActionButton,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
getTerminalBuffer,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
async function openArgumentForm() {
|
||||||
|
await getRootAndWait()
|
||||||
|
const btn = await getActionButton(webdriver, 'Test suggestionsBrowserKey')
|
||||||
|
await btn.click()
|
||||||
|
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for argument form page', async () => {
|
||||||
|
const url = await webdriver.getCurrentUrl()
|
||||||
|
return url.includes('/actionBinding/') && url.includes('/argumentForm')
|
||||||
|
}),
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTestInput() {
|
||||||
|
return await webdriver.findElement(By.id('testInput'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTestInput2() {
|
||||||
|
return await webdriver.findElement(By.id('testInput2'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDatalistOptions(inputName = 'testInput') {
|
||||||
|
return await webdriver.findElements(By.css(`datalist#${inputName}-choices option`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
const submitButton = await webdriver.findElement(By.css('button[name="start"]'))
|
||||||
|
await submitButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForLogsPage() {
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for logs page', async () => {
|
||||||
|
const url = await webdriver.getCurrentUrl()
|
||||||
|
return url.includes('/logs/') && !url.endsWith('/logs')
|
||||||
|
}),
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForExecutionComplete() {
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for execution status', async () => {
|
||||||
|
const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
|
||||||
|
return statusElements.length > 0
|
||||||
|
}),
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for execution to finish', async () => {
|
||||||
|
try {
|
||||||
|
const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
|
||||||
|
const statusText = await statusElement.getText()
|
||||||
|
return !statusText.includes('Executing')
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
|
||||||
|
await webdriver.sleep(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLocalStorageItem(key) {
|
||||||
|
return await webdriver.executeScript(`return localStorage.getItem('${key}')`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearLocalStorage() {
|
||||||
|
await webdriver.executeScript('return localStorage.clear()')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('config: suggestionsBrowserKey', function () {
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('suggestionsBrowserKey')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Input fields with suggestionsBrowserKey are rendered', async function () {
|
||||||
|
await openArgumentForm()
|
||||||
|
|
||||||
|
const input1 = await getTestInput()
|
||||||
|
expect(await input1.getTagName()).to.equal('input')
|
||||||
|
expect(await input1.getAttribute('type')).to.equal('text')
|
||||||
|
|
||||||
|
const label1 = await webdriver.findElement(By.css('label[for="testInput"]'))
|
||||||
|
expect(await label1.getText()).to.contain('Test Input')
|
||||||
|
|
||||||
|
const input2 = await getTestInput2()
|
||||||
|
expect(await input2.getTagName()).to.equal('input')
|
||||||
|
expect(await input2.getAttribute('type')).to.equal('text')
|
||||||
|
|
||||||
|
const label2 = await webdriver.findElement(By.css('label[for="testInput2"]'))
|
||||||
|
expect(await label2.getText()).to.contain('Test Input 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Submitting form saves value to localStorage', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
|
||||||
|
// Clear localStorage first
|
||||||
|
await clearLocalStorage()
|
||||||
|
|
||||||
|
await openArgumentForm()
|
||||||
|
|
||||||
|
const input = await getTestInput()
|
||||||
|
const testValue = 'test-value-123'
|
||||||
|
await input.clear()
|
||||||
|
await input.sendKeys(testValue)
|
||||||
|
|
||||||
|
await submitForm()
|
||||||
|
await waitForLogsPage()
|
||||||
|
await waitForExecutionComplete()
|
||||||
|
|
||||||
|
// Verify value was saved to localStorage
|
||||||
|
const stored = await getLocalStorageItem('olivetin-suggestions-test-suggestions-key')
|
||||||
|
expect(stored).to.not.be.null
|
||||||
|
|
||||||
|
const suggestions = JSON.parse(stored)
|
||||||
|
expect(suggestions).to.be.an('array')
|
||||||
|
expect(suggestions).to.include(testValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Previously saved values appear in datalist', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
|
||||||
|
// First, save a value to localStorage
|
||||||
|
const testValue = 'saved-suggestion-456'
|
||||||
|
await webdriver.executeScript(`
|
||||||
|
const key = 'olivetin-suggestions-test-suggestions-key';
|
||||||
|
localStorage.setItem(key, JSON.stringify(['${testValue}']));
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Open the form
|
||||||
|
await openArgumentForm()
|
||||||
|
|
||||||
|
// Check that datalist exists and contains the saved value
|
||||||
|
const datalist = await webdriver.findElement(By.id('testInput-choices'))
|
||||||
|
expect(datalist).to.not.be.null
|
||||||
|
|
||||||
|
const options = await getDatalistOptions()
|
||||||
|
expect(options.length).to.be.greaterThan(0)
|
||||||
|
|
||||||
|
// Check if the saved value appears in the datalist
|
||||||
|
let foundValue = false
|
||||||
|
for (const option of options) {
|
||||||
|
const value = await option.getAttribute('value')
|
||||||
|
if (value === testValue) {
|
||||||
|
foundValue = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundValue).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Multiple submissions accumulate suggestions', async function () {
|
||||||
|
this.timeout(20000)
|
||||||
|
|
||||||
|
// Clear localStorage first
|
||||||
|
await clearLocalStorage()
|
||||||
|
|
||||||
|
// Submit first value
|
||||||
|
await openArgumentForm()
|
||||||
|
const input1 = await getTestInput()
|
||||||
|
await input1.clear()
|
||||||
|
await input1.sendKeys('first-value')
|
||||||
|
await submitForm()
|
||||||
|
await waitForLogsPage()
|
||||||
|
await waitForExecutionComplete()
|
||||||
|
|
||||||
|
// Submit second value
|
||||||
|
await openArgumentForm()
|
||||||
|
const input2 = await getTestInput()
|
||||||
|
await input2.clear()
|
||||||
|
await input2.sendKeys('second-value')
|
||||||
|
await submitForm()
|
||||||
|
await waitForLogsPage()
|
||||||
|
await waitForExecutionComplete()
|
||||||
|
|
||||||
|
// Verify both values are in localStorage
|
||||||
|
const stored = await getLocalStorageItem('olivetin-suggestions-test-suggestions-key')
|
||||||
|
expect(stored).to.not.be.null
|
||||||
|
|
||||||
|
const suggestions = JSON.parse(stored)
|
||||||
|
expect(suggestions).to.be.an('array')
|
||||||
|
expect(suggestions).to.include('first-value')
|
||||||
|
expect(suggestions).to.include('second-value')
|
||||||
|
expect(suggestions[0]).to.equal('second-value') // Most recent should be first
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Empty values are not saved to localStorage', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
|
||||||
|
// Clear localStorage first
|
||||||
|
await clearLocalStorage()
|
||||||
|
|
||||||
|
await openArgumentForm()
|
||||||
|
|
||||||
|
const input = await getTestInput()
|
||||||
|
// Leave input empty (or clear it if it has a default)
|
||||||
|
await input.clear()
|
||||||
|
|
||||||
|
await submitForm()
|
||||||
|
await waitForLogsPage()
|
||||||
|
await waitForExecutionComplete()
|
||||||
|
|
||||||
|
// Verify empty value was not saved - localStorage should be null or empty-equivalent
|
||||||
|
const stored = await getLocalStorageItem('olivetin-suggestions-test-suggestions-key')
|
||||||
|
// Should be null OR empty JSON array string ("[]") OR parse to empty array
|
||||||
|
if (stored !== null) {
|
||||||
|
const suggestions = JSON.parse(stored)
|
||||||
|
expect(suggestions).to.be.an('array')
|
||||||
|
expect(suggestions).to.have.length(0)
|
||||||
|
}
|
||||||
|
// If stored is null, that's also acceptable - no assertion needed
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Suggestions are shared across inputs with the same suggestionsBrowserKey', async function () {
|
||||||
|
this.timeout(20000)
|
||||||
|
|
||||||
|
// Clear localStorage first
|
||||||
|
await clearLocalStorage()
|
||||||
|
|
||||||
|
// Submit a value using the first input
|
||||||
|
await openArgumentForm()
|
||||||
|
const input1 = await getTestInput()
|
||||||
|
await input1.clear()
|
||||||
|
await input1.sendKeys('shared-value-from-input1')
|
||||||
|
await submitForm()
|
||||||
|
await waitForLogsPage()
|
||||||
|
await waitForExecutionComplete()
|
||||||
|
|
||||||
|
// Open the form again and verify the value appears in both datalists
|
||||||
|
await openArgumentForm()
|
||||||
|
|
||||||
|
// Check first input's datalist
|
||||||
|
const datalist1 = await webdriver.findElement(By.id('testInput-choices'))
|
||||||
|
expect(datalist1).to.not.be.null
|
||||||
|
const options1 = await getDatalistOptions('testInput')
|
||||||
|
let foundInInput1 = false
|
||||||
|
for (const option of options1) {
|
||||||
|
const value = await option.getAttribute('value')
|
||||||
|
if (value === 'shared-value-from-input1') {
|
||||||
|
foundInInput1 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundInInput1).to.be.true
|
||||||
|
|
||||||
|
// Check second input's datalist
|
||||||
|
const datalist2 = await webdriver.findElement(By.id('testInput2-choices'))
|
||||||
|
expect(datalist2).to.not.be.null
|
||||||
|
const options2 = await getDatalistOptions('testInput2')
|
||||||
|
let foundInInput2 = false
|
||||||
|
for (const option of options2) {
|
||||||
|
const value = await option.getAttribute('value')
|
||||||
|
if (value === 'shared-value-from-input1') {
|
||||||
|
foundInInput2 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundInInput2).to.be.true
|
||||||
|
|
||||||
|
// Now submit a value using the second input
|
||||||
|
const input2 = await getTestInput2()
|
||||||
|
await input2.clear()
|
||||||
|
await input2.sendKeys('shared-value-from-input2')
|
||||||
|
await submitForm()
|
||||||
|
await waitForLogsPage()
|
||||||
|
await waitForExecutionComplete()
|
||||||
|
|
||||||
|
// Verify both values appear in both datalists
|
||||||
|
await openArgumentForm()
|
||||||
|
|
||||||
|
// Check that both values are in the first input's datalist
|
||||||
|
const options1After = await getDatalistOptions('testInput')
|
||||||
|
let foundValue1 = false
|
||||||
|
let foundValue2 = false
|
||||||
|
for (const option of options1After) {
|
||||||
|
const value = await option.getAttribute('value')
|
||||||
|
if (value === 'shared-value-from-input1') {
|
||||||
|
foundValue1 = true
|
||||||
|
}
|
||||||
|
if (value === 'shared-value-from-input2') {
|
||||||
|
foundValue2 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundValue1).to.be.true
|
||||||
|
expect(foundValue2).to.be.true
|
||||||
|
|
||||||
|
// Check that both values are in the second input's datalist
|
||||||
|
const options2After = await getDatalistOptions('testInput2')
|
||||||
|
foundValue1 = false
|
||||||
|
foundValue2 = false
|
||||||
|
for (const option of options2After) {
|
||||||
|
const value = await option.getAttribute('value')
|
||||||
|
if (value === 'shared-value-from-input1') {
|
||||||
|
foundValue1 = true
|
||||||
|
}
|
||||||
|
if (value === 'shared-value-from-input2') {
|
||||||
|
foundValue2 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundValue1).to.be.true
|
||||||
|
expect(foundValue2).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -13,6 +13,7 @@ message Action {
|
||||||
string popup_on_start = 6;
|
string popup_on_start = 6;
|
||||||
int32 order = 7;
|
int32 order = 7;
|
||||||
int32 timeout = 8;
|
int32 timeout = 8;
|
||||||
|
string datetime_rate_limit_expires = 9; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
}
|
}
|
||||||
|
|
||||||
message ActionArgument {
|
message ActionArgument {
|
||||||
|
|
@ -25,6 +26,7 @@ message ActionArgument {
|
||||||
|
|
||||||
string description = 6;
|
string description = 6;
|
||||||
map<string, string> suggestions = 7;
|
map<string, string> suggestions = 7;
|
||||||
|
string suggestions_browser_key = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ActionArgumentChoice {
|
message ActionArgumentChoice {
|
||||||
|
|
@ -133,12 +135,13 @@ message LogEntry {
|
||||||
repeated string tags = 10;
|
repeated string tags = 10;
|
||||||
string execution_tracking_id = 11;
|
string execution_tracking_id = 11;
|
||||||
string datetime_finished = 12;
|
string datetime_finished = 12;
|
||||||
string action_id = 13;
|
|
||||||
bool execution_started = 14;
|
bool execution_started = 14;
|
||||||
bool execution_finished = 15;
|
bool execution_finished = 15;
|
||||||
bool blocked = 16;
|
bool blocked = 16;
|
||||||
int64 datetime_index = 17;
|
int64 datetime_index = 17;
|
||||||
bool can_kill = 18;
|
bool can_kill = 18;
|
||||||
|
string datetime_rate_limit_expires = 19; // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
|
string binding_id = 20; // Binding ID for matching rate limits to action buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetLogsResponse {
|
message GetLogsResponse {
|
||||||
|
|
@ -216,7 +219,7 @@ message DumpVarsResponse {
|
||||||
map<string, string> contents = 2;
|
map<string, string> contents = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ActionEntityPair {
|
message DebugBinding {
|
||||||
string action_title = 1;
|
string action_title = 1;
|
||||||
string entity_prefix = 2;
|
string entity_prefix = 2;
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +227,7 @@ message ActionEntityPair {
|
||||||
message DumpPublicIdActionMapRequest {}
|
message DumpPublicIdActionMapRequest {}
|
||||||
message DumpPublicIdActionMapResponse {
|
message DumpPublicIdActionMapResponse {
|
||||||
string alert = 1;
|
string alert = 1;
|
||||||
map<string, ActionEntityPair> contents = 2;
|
map<string, DebugBinding> contents = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetReadyzRequest {}
|
message GetReadyzRequest {}
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,18 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Action struct {
|
type Action struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
|
BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
|
||||||
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||||
CanExec bool `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"`
|
CanExec bool `protobuf:"varint,4,opt,name=can_exec,json=canExec,proto3" json:"can_exec,omitempty"`
|
||||||
Arguments []*ActionArgument `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"`
|
Arguments []*ActionArgument `protobuf:"bytes,5,rep,name=arguments,proto3" json:"arguments,omitempty"`
|
||||||
PopupOnStart string `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"`
|
PopupOnStart string `protobuf:"bytes,6,opt,name=popup_on_start,json=popupOnStart,proto3" json:"popup_on_start,omitempty"`
|
||||||
Order int32 `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"`
|
Order int32 `protobuf:"varint,7,opt,name=order,proto3" json:"order,omitempty"`
|
||||||
Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
DatetimeRateLimitExpires string `protobuf:"bytes,9,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
sizeCache protoimpl.SizeCache
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Action) Reset() {
|
func (x *Action) Reset() {
|
||||||
|
|
@ -121,17 +122,25 @@ func (x *Action) GetTimeout() int32 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Action) GetDatetimeRateLimitExpires() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.DatetimeRateLimitExpires
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type ActionArgument struct {
|
type ActionArgument struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
DefaultValue string `protobuf:"bytes,4,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"`
|
DefaultValue string `protobuf:"bytes,4,opt,name=default_value,json=defaultValue,proto3" json:"default_value,omitempty"`
|
||||||
Choices []*ActionArgumentChoice `protobuf:"bytes,5,rep,name=choices,proto3" json:"choices,omitempty"`
|
Choices []*ActionArgumentChoice `protobuf:"bytes,5,rep,name=choices,proto3" json:"choices,omitempty"`
|
||||||
Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
|
Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"`
|
||||||
Suggestions map[string]string `protobuf:"bytes,7,rep,name=suggestions,proto3" json:"suggestions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
Suggestions map[string]string `protobuf:"bytes,7,rep,name=suggestions,proto3" json:"suggestions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||||
unknownFields protoimpl.UnknownFields
|
SuggestionsBrowserKey string `protobuf:"bytes,8,opt,name=suggestions_browser_key,json=suggestionsBrowserKey,proto3" json:"suggestions_browser_key,omitempty"`
|
||||||
sizeCache protoimpl.SizeCache
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ActionArgument) Reset() {
|
func (x *ActionArgument) Reset() {
|
||||||
|
|
@ -213,6 +222,13 @@ func (x *ActionArgument) GetSuggestions() map[string]string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ActionArgument) GetSuggestionsBrowserKey() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.SuggestionsBrowserKey
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type ActionArgumentChoice struct {
|
type ActionArgumentChoice struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
|
@ -1138,26 +1154,27 @@ func (x *GetLogsRequest) GetDateFilter() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogEntry struct {
|
type LogEntry struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
|
DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
|
||||||
ActionTitle string `protobuf:"bytes,2,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
|
ActionTitle string `protobuf:"bytes,2,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
|
||||||
Output string `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"`
|
Output string `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"`
|
||||||
TimedOut bool `protobuf:"varint,5,opt,name=timed_out,json=timedOut,proto3" json:"timed_out,omitempty"`
|
TimedOut bool `protobuf:"varint,5,opt,name=timed_out,json=timedOut,proto3" json:"timed_out,omitempty"`
|
||||||
ExitCode int32 `protobuf:"varint,6,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"`
|
ExitCode int32 `protobuf:"varint,6,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"`
|
||||||
User string `protobuf:"bytes,7,opt,name=user,proto3" json:"user,omitempty"`
|
User string `protobuf:"bytes,7,opt,name=user,proto3" json:"user,omitempty"`
|
||||||
UserClass string `protobuf:"bytes,8,opt,name=user_class,json=userClass,proto3" json:"user_class,omitempty"`
|
UserClass string `protobuf:"bytes,8,opt,name=user_class,json=userClass,proto3" json:"user_class,omitempty"`
|
||||||
ActionIcon string `protobuf:"bytes,9,opt,name=action_icon,json=actionIcon,proto3" json:"action_icon,omitempty"`
|
ActionIcon string `protobuf:"bytes,9,opt,name=action_icon,json=actionIcon,proto3" json:"action_icon,omitempty"`
|
||||||
Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"`
|
Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||||
ExecutionTrackingId string `protobuf:"bytes,11,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
|
ExecutionTrackingId string `protobuf:"bytes,11,opt,name=execution_tracking_id,json=executionTrackingId,proto3" json:"execution_tracking_id,omitempty"`
|
||||||
DatetimeFinished string `protobuf:"bytes,12,opt,name=datetime_finished,json=datetimeFinished,proto3" json:"datetime_finished,omitempty"`
|
DatetimeFinished string `protobuf:"bytes,12,opt,name=datetime_finished,json=datetimeFinished,proto3" json:"datetime_finished,omitempty"`
|
||||||
ActionId string `protobuf:"bytes,13,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
|
ExecutionStarted bool `protobuf:"varint,14,opt,name=execution_started,json=executionStarted,proto3" json:"execution_started,omitempty"`
|
||||||
ExecutionStarted bool `protobuf:"varint,14,opt,name=execution_started,json=executionStarted,proto3" json:"execution_started,omitempty"`
|
ExecutionFinished bool `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"`
|
||||||
ExecutionFinished bool `protobuf:"varint,15,opt,name=execution_finished,json=executionFinished,proto3" json:"execution_finished,omitempty"`
|
Blocked bool `protobuf:"varint,16,opt,name=blocked,proto3" json:"blocked,omitempty"`
|
||||||
Blocked bool `protobuf:"varint,16,opt,name=blocked,proto3" json:"blocked,omitempty"`
|
DatetimeIndex int64 `protobuf:"varint,17,opt,name=datetime_index,json=datetimeIndex,proto3" json:"datetime_index,omitempty"`
|
||||||
DatetimeIndex int64 `protobuf:"varint,17,opt,name=datetime_index,json=datetimeIndex,proto3" json:"datetime_index,omitempty"`
|
CanKill bool `protobuf:"varint,18,opt,name=can_kill,json=canKill,proto3" json:"can_kill,omitempty"`
|
||||||
CanKill bool `protobuf:"varint,18,opt,name=can_kill,json=canKill,proto3" json:"can_kill,omitempty"`
|
DatetimeRateLimitExpires string `protobuf:"bytes,19,opt,name=datetime_rate_limit_expires,json=datetimeRateLimitExpires,proto3" json:"datetime_rate_limit_expires,omitempty"` // Datetime when rate limit expires (empty string if not rate limited), format: "2006-01-02 15:04:05"
|
||||||
unknownFields protoimpl.UnknownFields
|
BindingId string `protobuf:"bytes,20,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` // Binding ID for matching rate limits to action buttons
|
||||||
sizeCache protoimpl.SizeCache
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LogEntry) Reset() {
|
func (x *LogEntry) Reset() {
|
||||||
|
|
@ -1267,13 +1284,6 @@ func (x *LogEntry) GetDatetimeFinished() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LogEntry) GetActionId() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.ActionId
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *LogEntry) GetExecutionStarted() bool {
|
func (x *LogEntry) GetExecutionStarted() bool {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ExecutionStarted
|
return x.ExecutionStarted
|
||||||
|
|
@ -1309,6 +1319,20 @@ func (x *LogEntry) GetCanKill() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LogEntry) GetDatetimeRateLimitExpires() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.DatetimeRateLimitExpires
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LogEntry) GetBindingId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.BindingId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type GetLogsResponse struct {
|
type GetLogsResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Logs []*LogEntry `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"`
|
Logs []*LogEntry `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"`
|
||||||
|
|
@ -2097,7 +2121,7 @@ func (x *DumpVarsResponse) GetContents() map[string]string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionEntityPair struct {
|
type DebugBinding struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
ActionTitle string `protobuf:"bytes,1,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
|
ActionTitle string `protobuf:"bytes,1,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
|
||||||
EntityPrefix string `protobuf:"bytes,2,opt,name=entity_prefix,json=entityPrefix,proto3" json:"entity_prefix,omitempty"`
|
EntityPrefix string `protobuf:"bytes,2,opt,name=entity_prefix,json=entityPrefix,proto3" json:"entity_prefix,omitempty"`
|
||||||
|
|
@ -2105,20 +2129,20 @@ type ActionEntityPair struct {
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ActionEntityPair) Reset() {
|
func (x *DebugBinding) Reset() {
|
||||||
*x = ActionEntityPair{}
|
*x = DebugBinding{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ActionEntityPair) String() string {
|
func (x *DebugBinding) String() string {
|
||||||
return protoimpl.X.MessageStringOf(x)
|
return protoimpl.X.MessageStringOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ActionEntityPair) ProtoMessage() {}
|
func (*DebugBinding) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ActionEntityPair) ProtoReflect() protoreflect.Message {
|
func (x *DebugBinding) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[35]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
|
@ -2130,19 +2154,19 @@ func (x *ActionEntityPair) ProtoReflect() protoreflect.Message {
|
||||||
return mi.MessageOf(x)
|
return mi.MessageOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: Use ActionEntityPair.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DebugBinding.ProtoReflect.Descriptor instead.
|
||||||
func (*ActionEntityPair) Descriptor() ([]byte, []int) {
|
func (*DebugBinding) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{35}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{35}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ActionEntityPair) GetActionTitle() string {
|
func (x *DebugBinding) GetActionTitle() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.ActionTitle
|
return x.ActionTitle
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ActionEntityPair) GetEntityPrefix() string {
|
func (x *DebugBinding) GetEntityPrefix() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.EntityPrefix
|
return x.EntityPrefix
|
||||||
}
|
}
|
||||||
|
|
@ -2186,9 +2210,9 @@ func (*DumpPublicIdActionMapRequest) Descriptor() ([]byte, []int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DumpPublicIdActionMapResponse struct {
|
type DumpPublicIdActionMapResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Alert string `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
|
Alert string `protobuf:"bytes,1,opt,name=alert,proto3" json:"alert,omitempty"`
|
||||||
Contents map[string]*ActionEntityPair `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
Contents map[string]*DebugBinding `protobuf:"bytes,2,rep,name=contents,proto3" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -2230,7 +2254,7 @@ func (x *DumpPublicIdActionMapResponse) GetAlert() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DumpPublicIdActionMapResponse) GetContents() map[string]*ActionEntityPair {
|
func (x *DumpPublicIdActionMapResponse) GetContents() map[string]*DebugBinding {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Contents
|
return x.Contents
|
||||||
}
|
}
|
||||||
|
|
@ -3847,7 +3871,7 @@ var File_olivetin_api_v1_olivetin_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\x81\x02\n" +
|
"\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\xc0\x02\n" +
|
||||||
"\x06Action\x12\x1d\n" +
|
"\x06Action\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"binding_id\x18\x01 \x01(\tR\tbindingId\x12\x14\n" +
|
"binding_id\x18\x01 \x01(\tR\tbindingId\x12\x14\n" +
|
||||||
|
|
@ -3857,7 +3881,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\targuments\x18\x05 \x03(\v2\x1f.olivetin.api.v1.ActionArgumentR\targuments\x12$\n" +
|
"\targuments\x18\x05 \x03(\v2\x1f.olivetin.api.v1.ActionArgumentR\targuments\x12$\n" +
|
||||||
"\x0epopup_on_start\x18\x06 \x01(\tR\fpopupOnStart\x12\x14\n" +
|
"\x0epopup_on_start\x18\x06 \x01(\tR\fpopupOnStart\x12\x14\n" +
|
||||||
"\x05order\x18\a \x01(\x05R\x05order\x12\x18\n" +
|
"\x05order\x18\a \x01(\x05R\x05order\x12\x18\n" +
|
||||||
"\atimeout\x18\b \x01(\x05R\atimeout\"\xea\x02\n" +
|
"\atimeout\x18\b \x01(\x05R\atimeout\x12=\n" +
|
||||||
|
"\x1bdatetime_rate_limit_expires\x18\t \x01(\tR\x18datetimeRateLimitExpires\"\xa2\x03\n" +
|
||||||
"\x0eActionArgument\x12\x12\n" +
|
"\x0eActionArgument\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
|
||||||
"\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" +
|
"\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" +
|
||||||
|
|
@ -3865,7 +3890,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\rdefault_value\x18\x04 \x01(\tR\fdefaultValue\x12?\n" +
|
"\rdefault_value\x18\x04 \x01(\tR\fdefaultValue\x12?\n" +
|
||||||
"\achoices\x18\x05 \x03(\v2%.olivetin.api.v1.ActionArgumentChoiceR\achoices\x12 \n" +
|
"\achoices\x18\x05 \x03(\v2%.olivetin.api.v1.ActionArgumentChoiceR\achoices\x12 \n" +
|
||||||
"\vdescription\x18\x06 \x01(\tR\vdescription\x12R\n" +
|
"\vdescription\x18\x06 \x01(\tR\vdescription\x12R\n" +
|
||||||
"\vsuggestions\x18\a \x03(\v20.olivetin.api.v1.ActionArgument.SuggestionsEntryR\vsuggestions\x1a>\n" +
|
"\vsuggestions\x18\a \x03(\v20.olivetin.api.v1.ActionArgument.SuggestionsEntryR\vsuggestions\x126\n" +
|
||||||
|
"\x17suggestions_browser_key\x18\b \x01(\tR\x15suggestionsBrowserKey\x1a>\n" +
|
||||||
"\x10SuggestionsEntry\x12\x10\n" +
|
"\x10SuggestionsEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
|
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
|
||||||
|
|
@ -3934,7 +3960,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\x0eGetLogsRequest\x12!\n" +
|
"\x0eGetLogsRequest\x12!\n" +
|
||||||
"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
|
"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
|
||||||
"\vdate_filter\x18\x02 \x01(\tR\n" +
|
"\vdate_filter\x18\x02 \x01(\tR\n" +
|
||||||
"dateFilter\"\xc8\x04\n" +
|
"dateFilter\"\x89\x05\n" +
|
||||||
"\bLogEntry\x12)\n" +
|
"\bLogEntry\x12)\n" +
|
||||||
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
|
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
|
||||||
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
|
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
|
||||||
|
|
@ -3949,13 +3975,15 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\x04tags\x18\n" +
|
"\x04tags\x18\n" +
|
||||||
" \x03(\tR\x04tags\x122\n" +
|
" \x03(\tR\x04tags\x122\n" +
|
||||||
"\x15execution_tracking_id\x18\v \x01(\tR\x13executionTrackingId\x12+\n" +
|
"\x15execution_tracking_id\x18\v \x01(\tR\x13executionTrackingId\x12+\n" +
|
||||||
"\x11datetime_finished\x18\f \x01(\tR\x10datetimeFinished\x12\x1b\n" +
|
"\x11datetime_finished\x18\f \x01(\tR\x10datetimeFinished\x12+\n" +
|
||||||
"\taction_id\x18\r \x01(\tR\bactionId\x12+\n" +
|
|
||||||
"\x11execution_started\x18\x0e \x01(\bR\x10executionStarted\x12-\n" +
|
"\x11execution_started\x18\x0e \x01(\bR\x10executionStarted\x12-\n" +
|
||||||
"\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" +
|
"\x12execution_finished\x18\x0f \x01(\bR\x11executionFinished\x12\x18\n" +
|
||||||
"\ablocked\x18\x10 \x01(\bR\ablocked\x12%\n" +
|
"\ablocked\x18\x10 \x01(\bR\ablocked\x12%\n" +
|
||||||
"\x0edatetime_index\x18\x11 \x01(\x03R\rdatetimeIndex\x12\x19\n" +
|
"\x0edatetime_index\x18\x11 \x01(\x03R\rdatetimeIndex\x12\x19\n" +
|
||||||
"\bcan_kill\x18\x12 \x01(\bR\acanKill\"\xca\x01\n" +
|
"\bcan_kill\x18\x12 \x01(\bR\acanKill\x12=\n" +
|
||||||
|
"\x1bdatetime_rate_limit_expires\x18\x13 \x01(\tR\x18datetimeRateLimitExpires\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"binding_id\x18\x14 \x01(\tR\tbindingId\"\xca\x01\n" +
|
||||||
"\x0fGetLogsResponse\x12-\n" +
|
"\x0fGetLogsResponse\x12-\n" +
|
||||||
"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" +
|
"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" +
|
||||||
"\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +
|
"\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +
|
||||||
|
|
@ -4007,17 +4035,17 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\bcontents\x18\x02 \x03(\v2/.olivetin.api.v1.DumpVarsResponse.ContentsEntryR\bcontents\x1a;\n" +
|
"\bcontents\x18\x02 \x03(\v2/.olivetin.api.v1.DumpVarsResponse.ContentsEntryR\bcontents\x1a;\n" +
|
||||||
"\rContentsEntry\x12\x10\n" +
|
"\rContentsEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Z\n" +
|
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"V\n" +
|
||||||
"\x10ActionEntityPair\x12!\n" +
|
"\fDebugBinding\x12!\n" +
|
||||||
"\faction_title\x18\x01 \x01(\tR\vactionTitle\x12#\n" +
|
"\faction_title\x18\x01 \x01(\tR\vactionTitle\x12#\n" +
|
||||||
"\rentity_prefix\x18\x02 \x01(\tR\fentityPrefix\"\x1e\n" +
|
"\rentity_prefix\x18\x02 \x01(\tR\fentityPrefix\"\x1e\n" +
|
||||||
"\x1cDumpPublicIdActionMapRequest\"\xef\x01\n" +
|
"\x1cDumpPublicIdActionMapRequest\"\xeb\x01\n" +
|
||||||
"\x1dDumpPublicIdActionMapResponse\x12\x14\n" +
|
"\x1dDumpPublicIdActionMapResponse\x12\x14\n" +
|
||||||
"\x05alert\x18\x01 \x01(\tR\x05alert\x12X\n" +
|
"\x05alert\x18\x01 \x01(\tR\x05alert\x12X\n" +
|
||||||
"\bcontents\x18\x02 \x03(\v2<.olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntryR\bcontents\x1a^\n" +
|
"\bcontents\x18\x02 \x03(\v2<.olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntryR\bcontents\x1aZ\n" +
|
||||||
"\rContentsEntry\x12\x10\n" +
|
"\rContentsEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x127\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x123\n" +
|
||||||
"\x05value\x18\x02 \x01(\v2!.olivetin.api.v1.ActionEntityPairR\x05value:\x028\x01\"\x12\n" +
|
"\x05value\x18\x02 \x01(\v2\x1d.olivetin.api.v1.DebugBindingR\x05value:\x028\x01\"\x12\n" +
|
||||||
"\x10GetReadyzRequest\"+\n" +
|
"\x10GetReadyzRequest\"+\n" +
|
||||||
"\x11GetReadyzResponse\x12\x16\n" +
|
"\x11GetReadyzResponse\x12\x16\n" +
|
||||||
"\x06status\x18\x01 \x01(\tR\x06status\"\x14\n" +
|
"\x06status\x18\x01 \x01(\tR\x06status\"\x14\n" +
|
||||||
|
|
@ -4191,7 +4219,7 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
|
||||||
(*SosReportResponse)(nil), // 32: olivetin.api.v1.SosReportResponse
|
(*SosReportResponse)(nil), // 32: olivetin.api.v1.SosReportResponse
|
||||||
(*DumpVarsRequest)(nil), // 33: olivetin.api.v1.DumpVarsRequest
|
(*DumpVarsRequest)(nil), // 33: olivetin.api.v1.DumpVarsRequest
|
||||||
(*DumpVarsResponse)(nil), // 34: olivetin.api.v1.DumpVarsResponse
|
(*DumpVarsResponse)(nil), // 34: olivetin.api.v1.DumpVarsResponse
|
||||||
(*ActionEntityPair)(nil), // 35: olivetin.api.v1.ActionEntityPair
|
(*DebugBinding)(nil), // 35: olivetin.api.v1.DebugBinding
|
||||||
(*DumpPublicIdActionMapRequest)(nil), // 36: olivetin.api.v1.DumpPublicIdActionMapRequest
|
(*DumpPublicIdActionMapRequest)(nil), // 36: olivetin.api.v1.DumpPublicIdActionMapRequest
|
||||||
(*DumpPublicIdActionMapResponse)(nil), // 37: olivetin.api.v1.DumpPublicIdActionMapResponse
|
(*DumpPublicIdActionMapResponse)(nil), // 37: olivetin.api.v1.DumpPublicIdActionMapResponse
|
||||||
(*GetReadyzRequest)(nil), // 38: olivetin.api.v1.GetReadyzRequest
|
(*GetReadyzRequest)(nil), // 38: olivetin.api.v1.GetReadyzRequest
|
||||||
|
|
@ -4260,7 +4288,7 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
|
||||||
0, // 27: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
|
0, // 27: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
|
||||||
65, // 28: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
|
65, // 28: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
|
||||||
3, // 29: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
|
3, // 29: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
|
||||||
35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.ActionEntityPair
|
35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
|
||||||
6, // 31: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
|
6, // 31: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
|
||||||
9, // 32: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
|
9, // 32: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
|
||||||
12, // 33: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
|
12, // 33: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
auth "github.com/OliveTin/OliveTin/internal/auth"
|
auth "github.com/OliveTin/OliveTin/internal/auth"
|
||||||
|
|
@ -275,23 +276,37 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string {
|
||||||
|
if logEntry.Binding == nil || logEntry.Binding.Action == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
expiryUnix := api.executor.GetTimeUntilAvailable(logEntry.Binding)
|
||||||
|
if expiryUnix <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
|
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
|
||||||
pble := &apiv1.LogEntry{
|
pble := &apiv1.LogEntry{
|
||||||
ActionTitle: logEntry.ActionTitle,
|
ActionTitle: logEntry.ActionTitle,
|
||||||
ActionIcon: logEntry.ActionIcon,
|
ActionIcon: logEntry.ActionIcon,
|
||||||
ActionId: logEntry.ActionId,
|
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
||||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
||||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
DatetimeIndex: logEntry.Index,
|
||||||
DatetimeIndex: logEntry.Index,
|
Output: logEntry.Output,
|
||||||
Output: logEntry.Output,
|
TimedOut: logEntry.TimedOut,
|
||||||
TimedOut: logEntry.TimedOut,
|
Blocked: logEntry.Blocked,
|
||||||
Blocked: logEntry.Blocked,
|
ExitCode: logEntry.ExitCode,
|
||||||
ExitCode: logEntry.ExitCode,
|
Tags: logEntry.Tags,
|
||||||
Tags: logEntry.Tags,
|
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
||||||
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
ExecutionStarted: logEntry.ExecutionStarted,
|
||||||
ExecutionStarted: logEntry.ExecutionStarted,
|
ExecutionFinished: logEntry.ExecutionFinished,
|
||||||
ExecutionFinished: logEntry.ExecutionFinished,
|
User: logEntry.Username,
|
||||||
User: logEntry.Username,
|
BindingId: logEntry.Binding.ID,
|
||||||
|
DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pble.ExecutionFinished {
|
if !pble.ExecutionFinished {
|
||||||
|
|
@ -311,10 +326,20 @@ func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string
|
||||||
return logEntry
|
return logEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
|
// This is the actual action ID, not the binding ID.
|
||||||
|
func getMostRecentExecutionStatusByActionId(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
|
||||||
var ile *executor.InternalLogEntry
|
var ile *executor.InternalLogEntry
|
||||||
|
|
||||||
logs := api.executor.GetLogsByActionId(actionId)
|
binding := api.executor.FindBindingByID(actionId)
|
||||||
|
if binding == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := api.executor.GetLogsByBindingId(binding.ID)
|
||||||
|
|
||||||
|
if len(logs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(logs) == 0 {
|
if len(logs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -341,7 +366,7 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap
|
||||||
ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
|
ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
|
ile = getMostRecentExecutionStatusByActionId(api, req.Msg.ActionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ile == nil {
|
if ile == nil {
|
||||||
|
|
@ -543,7 +568,7 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
filtered := api.filterLogsByACL(api.executor.GetLogsByActionId(req.Msg.ActionId), user)
|
filtered := api.filterLogsByACL(api.executor.GetLogsByBindingId(req.Msg.ActionId), user)
|
||||||
page := paginate(int64(len(filtered)), api.cfg.LogHistoryPageSize, req.Msg.StartOffset)
|
page := paginate(int64(len(filtered)), api.cfg.LogHistoryPageSize, req.Msg.StartOffset)
|
||||||
if page.empty {
|
if page.empty {
|
||||||
return connect.NewResponse(buildEmptyPageResponse(page)), nil
|
return connect.NewResponse(buildEmptyPageResponse(page)), nil
|
||||||
|
|
@ -692,7 +717,7 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum
|
||||||
|
|
||||||
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
|
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
|
||||||
res := &apiv1.DumpPublicIdActionMapResponse{}
|
res := &apiv1.DumpPublicIdActionMapResponse{}
|
||||||
res.Contents = make(map[string]*apiv1.ActionEntityPair)
|
res.Contents = make(map[string]*apiv1.DebugBinding)
|
||||||
|
|
||||||
if !api.cfg.InsecureAllowDumpActionMap {
|
if !api.cfg.InsecureAllowDumpActionMap {
|
||||||
res.Alert = "Dumping Public IDs is disallowed."
|
res.Alert = "Dumping Public IDs is disallowed."
|
||||||
|
|
@ -700,16 +725,15 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Requ
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
api.executor.MapActionIdToBindingLock.RLock()
|
api.executor.MapActionBindingsLock.RLock()
|
||||||
|
|
||||||
for k, v := range api.executor.MapActionIdToBinding {
|
for k, v := range api.executor.MapActionBindings {
|
||||||
res.Contents[k] = &apiv1.ActionEntityPair{
|
res.Contents[k] = &apiv1.DebugBinding{
|
||||||
ActionTitle: v.Action.Title,
|
ActionTitle: v.Action.Title,
|
||||||
EntityPrefix: "?",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api.executor.MapActionIdToBindingLock.RUnlock()
|
api.executor.MapActionBindingsLock.RUnlock()
|
||||||
|
|
||||||
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
|
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
|
||||||
|
|
||||||
|
|
@ -817,7 +841,7 @@ func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
|
func (api *oliveTinAPI) OnExecutionFinished(ile *executor.InternalLogEntry) {
|
||||||
toRemove := []*streamingClient{}
|
toRemove := []*streamingClient{}
|
||||||
|
|
||||||
for _, client := range api.copyOfStreamingClients() {
|
for _, client := range api.copyOfStreamingClients() {
|
||||||
|
|
@ -825,7 +849,7 @@ func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
|
||||||
case client.channel <- &apiv1.EventStreamResponse{
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
||||||
ExecutionFinished: &apiv1.EventExecutionFinished{
|
ExecutionFinished: &apiv1.EventExecutionFinished{
|
||||||
LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
|
LogEntry: api.internalLogEntryToPb(ile, client.AuthenticatedUser),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}:
|
}:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
|
@ -27,10 +28,10 @@ func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action {
|
func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action {
|
||||||
rr.ex.MapActionIdToBindingLock.RLock()
|
rr.ex.MapActionBindingsLock.RLock()
|
||||||
defer rr.ex.MapActionIdToBindingLock.RUnlock()
|
defer rr.ex.MapActionBindingsLock.RUnlock()
|
||||||
|
|
||||||
for _, binding := range rr.ex.MapActionIdToBinding {
|
for _, binding := range rr.ex.MapActionBindings {
|
||||||
if binding.Action.Title != title {
|
if binding.Action.Title != title {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -110,25 +111,34 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
|
||||||
aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action)
|
aclCanExec := acl.IsAllowedExec(rr.cfg, rr.AuthenticatedUser, action)
|
||||||
enabledExprCanExec := evaluateEnabledExpression(action, actionBinding.Entity)
|
enabledExprCanExec := evaluateEnabledExpression(action, actionBinding.Entity)
|
||||||
|
|
||||||
|
// Calculate rate limit expiry time
|
||||||
|
expiryUnix := rr.ex.GetTimeUntilAvailable(actionBinding)
|
||||||
|
datetimeRateLimitExpires := ""
|
||||||
|
if expiryUnix > 0 {
|
||||||
|
datetimeRateLimitExpires = time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
btn := apiv1.Action{
|
btn := apiv1.Action{
|
||||||
BindingId: actionBinding.ID,
|
BindingId: actionBinding.ID,
|
||||||
Title: entities.ParseTemplateWith(action.Title, actionBinding.Entity),
|
Title: entities.ParseTemplateWith(action.Title, actionBinding.Entity),
|
||||||
Icon: entities.ParseTemplateWith(action.Icon, actionBinding.Entity),
|
Icon: entities.ParseTemplateWith(action.Icon, actionBinding.Entity),
|
||||||
CanExec: aclCanExec && enabledExprCanExec,
|
CanExec: aclCanExec && enabledExprCanExec,
|
||||||
PopupOnStart: action.PopupOnStart,
|
PopupOnStart: action.PopupOnStart,
|
||||||
Order: int32(actionBinding.ConfigOrder),
|
Order: int32(actionBinding.ConfigOrder),
|
||||||
Timeout: int32(action.Timeout),
|
Timeout: int32(action.Timeout),
|
||||||
|
DatetimeRateLimitExpires: datetimeRateLimitExpires,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfgArg := range action.Arguments {
|
for _, cfgArg := range action.Arguments {
|
||||||
pbArg := apiv1.ActionArgument{
|
pbArg := apiv1.ActionArgument{
|
||||||
Name: cfgArg.Name,
|
Name: cfgArg.Name,
|
||||||
Title: cfgArg.Title,
|
Title: cfgArg.Title,
|
||||||
Type: cfgArg.Type,
|
Type: cfgArg.Type,
|
||||||
Description: cfgArg.Description,
|
Description: cfgArg.Description,
|
||||||
DefaultValue: cfgArg.Default,
|
DefaultValue: cfgArg.Default,
|
||||||
Choices: buildChoices(cfgArg),
|
Choices: buildChoices(cfgArg),
|
||||||
Suggestions: cfgArg.Suggestions,
|
Suggestions: cfgArg.Suggestions,
|
||||||
|
SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.Arguments = append(btn.Arguments, &pbArg)
|
btn.Arguments = append(btn.Arguments, &pbArg)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*httptest.Server, apiv1connect.OliveTinApiServiceClient) {
|
func getNewTestServerAndClient(injectedConfig *config.Config) (*httptest.Server, apiv1connect.OliveTinApiServiceClient) {
|
||||||
ex := executor.DefaultExecutor(injectedConfig)
|
ex := executor.DefaultExecutor(injectedConfig)
|
||||||
ex.RebuildActionMap()
|
ex.RebuildActionMap()
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ func TestGetActionsAndStart(t *testing.T) {
|
||||||
ex := executor.DefaultExecutor(cfg)
|
ex := executor.DefaultExecutor(cfg)
|
||||||
ex.RebuildActionMap()
|
ex.RebuildActionMap()
|
||||||
|
|
||||||
conn, client := getNewTestServerAndClient(t, cfg)
|
conn, client := getNewTestServerAndClient(cfg)
|
||||||
|
|
||||||
respInit, errInit := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
|
respInit, errInit := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
|
||||||
respGetReady, errReady := client.GetReadyz(context.Background(), connect.NewRequest(&apiv1.GetReadyzRequest{}))
|
respGetReady, errReady := client.GetReadyz(context.Background(), connect.NewRequest(&apiv1.GetReadyzRequest{}))
|
||||||
|
|
@ -99,7 +99,7 @@ func TestGetActionsAndStart(t *testing.T) {
|
||||||
func TestGetEntities(t *testing.T) {
|
func TestGetEntities(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
|
|
||||||
ts, client := getNewTestServerAndClient(t, cfg)
|
ts, client := getNewTestServerAndClient(cfg)
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
setupTestEntities()
|
setupTestEntities()
|
||||||
|
|
@ -315,10 +315,10 @@ func TestBuildActionWithEnabledExpression(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func findBindingByTitle(ex *executor.Executor, title string) *executor.ActionBinding {
|
func findBindingByTitle(ex *executor.Executor, title string) *executor.ActionBinding {
|
||||||
ex.MapActionIdToBindingLock.RLock()
|
ex.MapActionBindingsLock.RLock()
|
||||||
defer ex.MapActionIdToBindingLock.RUnlock()
|
defer ex.MapActionBindingsLock.RUnlock()
|
||||||
|
|
||||||
for _, b := range ex.MapActionIdToBinding {
|
for _, b := range ex.MapActionBindings {
|
||||||
if b.Action.Title == title {
|
if b.Action.Title == title {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
|
||||||
Contents: make([]*apiv1.DashboardComponent, 0),
|
Contents: make([]*apiv1.DashboardComponent, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, binding := range rr.ex.MapActionIdToBinding {
|
for _, binding := range rr.ex.MapActionBindings {
|
||||||
if binding.Action.Hidden {
|
if binding.Action.Hidden {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,16 @@ type Action struct {
|
||||||
|
|
||||||
// ActionArgument objects appear on Actions.
|
// ActionArgument objects appear on Actions.
|
||||||
type ActionArgument struct {
|
type ActionArgument struct {
|
||||||
Name string `koanf:"name"`
|
Name string `koanf:"name"`
|
||||||
Title string `koanf:"title"`
|
Title string `koanf:"title"`
|
||||||
Description string `koanf:"description"`
|
Description string `koanf:"description"`
|
||||||
Type string `koanf:"type"`
|
Type string `koanf:"type"`
|
||||||
Default string `koanf:"default"`
|
Default string `koanf:"default"`
|
||||||
Choices []ActionArgumentChoice `koanf:"choices"`
|
Choices []ActionArgumentChoice `koanf:"choices"`
|
||||||
Entity string `koanf:"entity"`
|
Entity string `koanf:"entity"`
|
||||||
RejectNull bool `koanf:"rejectNull"`
|
RejectNull bool `koanf:"rejectNull"`
|
||||||
Suggestions map[string]string `koanf:"suggestions"`
|
Suggestions map[string]string `koanf:"suggestions"`
|
||||||
|
SuggestionsBrowserKey string `koanf:"suggestionsBrowserKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionArgumentChoice represents a predefined choice for an argument.
|
// ActionArgumentChoice represents a predefined choice for an argument.
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,12 @@ type ActionBinding struct {
|
||||||
type Executor struct {
|
type Executor struct {
|
||||||
logs map[string]*InternalLogEntry
|
logs map[string]*InternalLogEntry
|
||||||
logsTrackingIdsByDate []string
|
logsTrackingIdsByDate []string
|
||||||
LogsByActionId map[string][]*InternalLogEntry
|
LogsByBindingId map[string][]*InternalLogEntry
|
||||||
|
|
||||||
logmutex sync.RWMutex
|
logmutex sync.RWMutex
|
||||||
|
|
||||||
MapActionIdToBinding map[string]*ActionBinding
|
MapActionBindings map[string]*ActionBinding
|
||||||
MapActionIdToBindingLock sync.RWMutex
|
MapActionBindingsLock sync.RWMutex
|
||||||
|
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
|
|
||||||
|
|
@ -86,7 +86,6 @@ type ExecutionRequest struct {
|
||||||
// easily serializable.
|
// easily serializable.
|
||||||
type InternalLogEntry struct {
|
type InternalLogEntry struct {
|
||||||
Binding *ActionBinding
|
Binding *ActionBinding
|
||||||
BindingID string
|
|
||||||
DatetimeStarted time.Time
|
DatetimeStarted time.Time
|
||||||
DatetimeFinished time.Time
|
DatetimeFinished time.Time
|
||||||
Output string
|
Output string
|
||||||
|
|
@ -110,7 +109,6 @@ type InternalLogEntry struct {
|
||||||
*/
|
*/
|
||||||
ActionTitle string
|
ActionTitle string
|
||||||
ActionIcon string
|
ActionIcon string
|
||||||
ActionId string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type executorStepFunc func(*ExecutionRequest) bool
|
type executorStepFunc func(*ExecutionRequest) bool
|
||||||
|
|
@ -122,8 +120,8 @@ func DefaultExecutor(cfg *config.Config) *Executor {
|
||||||
e.Cfg = cfg
|
e.Cfg = cfg
|
||||||
e.logs = make(map[string]*InternalLogEntry)
|
e.logs = make(map[string]*InternalLogEntry)
|
||||||
e.logsTrackingIdsByDate = make([]string, 0)
|
e.logsTrackingIdsByDate = make([]string, 0)
|
||||||
e.LogsByActionId = make(map[string][]*InternalLogEntry)
|
e.LogsByBindingId = make(map[string][]*InternalLogEntry)
|
||||||
e.MapActionIdToBinding = make(map[string]*ActionBinding)
|
e.MapActionBindings = make(map[string]*ActionBinding)
|
||||||
|
|
||||||
e.chainOfCommand = []executorStepFunc{
|
e.chainOfCommand = []executorStepFunc{
|
||||||
stepRequestAction,
|
stepRequestAction,
|
||||||
|
|
@ -315,10 +313,10 @@ func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
|
||||||
return entry, found
|
return entry, found
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
|
func (e *Executor) GetLogsByBindingId(bindingId string) []*InternalLogEntry {
|
||||||
e.logmutex.RLock()
|
e.logmutex.RLock()
|
||||||
|
|
||||||
logs, found := e.LogsByActionId[actionId]
|
logs, found := e.LogsByBindingId[bindingId]
|
||||||
|
|
||||||
e.logmutex.RUnlock()
|
e.logmutex.RUnlock()
|
||||||
|
|
||||||
|
|
@ -329,6 +327,122 @@ func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
|
||||||
return logs
|
return logs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldCountExecution checks if a log entry should be counted for rate limiting.
|
||||||
|
func shouldCountExecution(logEntry *InternalLogEntry, windowStart time.Time) bool {
|
||||||
|
return !logEntry.Blocked && logEntry.DatetimeStarted.After(windowStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateOldestExecution updates the oldest execution time if this entry is older.
|
||||||
|
func updateOldestExecution(oldestExecutionTime **time.Time, logEntry *InternalLogEntry) {
|
||||||
|
if *oldestExecutionTime == nil {
|
||||||
|
*oldestExecutionTime = &logEntry.DatetimeStarted
|
||||||
|
} else if logEntry.DatetimeStarted.Before(**oldestExecutionTime) {
|
||||||
|
*oldestExecutionTime = &logEntry.DatetimeStarted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findOldestExecutionInWindow finds the oldest execution within the time window and counts executions.
|
||||||
|
// Returns the count of executions and the oldest execution time, or nil if none found.
|
||||||
|
func findOldestExecutionInWindow(logs []*InternalLogEntry, windowStart time.Time) (int, *time.Time) {
|
||||||
|
executions := 0
|
||||||
|
var oldestExecutionTime *time.Time
|
||||||
|
|
||||||
|
for _, logEntry := range logs {
|
||||||
|
if !shouldCountExecution(logEntry, windowStart) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
executions++
|
||||||
|
updateOldestExecution(&oldestExecutionTime, logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executions, oldestExecutionTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateExpiryTime calculates when the oldest execution will fall outside the rate limit window.
|
||||||
|
func calculateExpiryTime(oldestExecutionTime time.Time, duration time.Duration, now time.Time) time.Time {
|
||||||
|
expiryTime := oldestExecutionTime.Add(duration)
|
||||||
|
if !expiryTime.After(now) {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateMaxExpiryTime updates maxExpiryTime if expiryTime is later.
|
||||||
|
func updateMaxExpiryTime(maxExpiryTime *time.Time, expiryTime time.Time) {
|
||||||
|
if expiryTime.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxExpiryTime.IsZero() || expiryTime.After(*maxExpiryTime) {
|
||||||
|
*maxExpiryTime = expiryTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateExpiryForRate calculates the expiry time for a single rate limit rule.
|
||||||
|
// Returns the expiry time if the rate limit is exceeded, or zero time if not.
|
||||||
|
func calculateExpiryForRate(rate config.RateSpec, logs []*InternalLogEntry, now time.Time) time.Time {
|
||||||
|
duration := parseDuration(rate)
|
||||||
|
if duration <= 0 {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
windowStart := now.Add(-duration)
|
||||||
|
executions, oldestExecutionTime := findOldestExecutionInWindow(logs, windowStart)
|
||||||
|
|
||||||
|
if executions < rate.Limit || oldestExecutionTime == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculateExpiryTime(*oldestExecutionTime, duration, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogsForBinding retrieves logs for a binding ID.
|
||||||
|
func (e *Executor) getLogsForBinding(bindingId string) []*InternalLogEntry {
|
||||||
|
e.logmutex.RLock()
|
||||||
|
logs, found := e.LogsByBindingId[bindingId]
|
||||||
|
e.logmutex.RUnlock()
|
||||||
|
|
||||||
|
if !found || len(logs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateMaxExpiryTimeFromRates calculates the maximum expiry time across all rate limit rules.
|
||||||
|
func calculateMaxExpiryTimeFromRates(rates []config.RateSpec, logs []*InternalLogEntry, now time.Time) time.Time {
|
||||||
|
var maxExpiryTime time.Time
|
||||||
|
|
||||||
|
for _, rate := range rates {
|
||||||
|
expiryTime := calculateExpiryForRate(rate, logs, now)
|
||||||
|
updateMaxExpiryTime(&maxExpiryTime, expiryTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxExpiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeUntilAvailable calculates when an action will be available again based on rate limits.
|
||||||
|
// Returns the Unix timestamp in seconds when the rate limit expires, or 0 if the action is available now.
|
||||||
|
func (e *Executor) GetTimeUntilAvailable(binding *ActionBinding) int64 {
|
||||||
|
if len(binding.Action.MaxRate) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := e.getLogsForBinding(binding.ID)
|
||||||
|
if logs == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
maxExpiryTime := calculateMaxExpiryTimeFromRates(binding.Action.MaxRate, logs, time.Now())
|
||||||
|
|
||||||
|
if maxExpiryTime.IsZero() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxExpiryTime.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
|
func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
|
||||||
e.logmutex.Lock()
|
e.logmutex.Lock()
|
||||||
|
|
||||||
|
|
@ -355,7 +469,6 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
|
||||||
ExitCode: DefaultExitCodeNotExecuted,
|
ExitCode: DefaultExitCodeNotExecuted,
|
||||||
ExecutionStarted: false,
|
ExecutionStarted: false,
|
||||||
ExecutionFinished: false,
|
ExecutionFinished: false,
|
||||||
ActionId: "",
|
|
||||||
ActionTitle: "notfound",
|
ActionTitle: "notfound",
|
||||||
ActionIcon: "💩",
|
ActionIcon: "💩",
|
||||||
Username: req.AuthenticatedUser.Username,
|
Username: req.AuthenticatedUser.Username,
|
||||||
|
|
@ -392,6 +505,11 @@ func (e *Executor) execChain(req *ExecutionRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure DatetimeFinished is set even if execution was blocked early
|
||||||
|
if req.logEntry.DatetimeFinished.IsZero() {
|
||||||
|
req.logEntry.DatetimeFinished = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
req.logEntry.ExecutionFinished = true
|
req.logEntry.ExecutionFinished = true
|
||||||
|
|
||||||
// This isn't a step, because we want to notify all listeners, irrespective
|
// This isn't a step, because we want to notify all listeners, irrespective
|
||||||
|
|
@ -404,7 +522,7 @@ func getConcurrentCount(req *ExecutionRequest) int {
|
||||||
|
|
||||||
req.executor.logmutex.RLock()
|
req.executor.logmutex.RLock()
|
||||||
|
|
||||||
for _, log := range req.executor.GetLogsByActionId(req.Binding.Action.ID) {
|
for _, log := range req.executor.GetLogsByBindingId(req.Binding.ID) {
|
||||||
if !log.ExecutionFinished {
|
if !log.ExecutionFinished {
|
||||||
concurrentCount += 1
|
concurrentCount += 1
|
||||||
}
|
}
|
||||||
|
|
@ -454,7 +572,7 @@ func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
|
||||||
|
|
||||||
then := time.Now().Add(-duration)
|
then := time.Now().Add(-duration)
|
||||||
|
|
||||||
for _, logEntry := range req.executor.GetLogsByActionId(req.Binding.Action.ID) {
|
for _, logEntry := range req.executor.GetLogsByBindingId(req.Binding.ID) {
|
||||||
// FIXME
|
// FIXME
|
||||||
/*
|
/*
|
||||||
if logEntry.EntityPrefix != req.EntityPrefix {
|
if logEntry.EntityPrefix != req.EntityPrefix {
|
||||||
|
|
@ -591,16 +709,15 @@ func stepRequestAction(req *ExecutionRequest) bool {
|
||||||
req.logEntry.ActionConfigTitle = req.Binding.Action.Title
|
req.logEntry.ActionConfigTitle = req.Binding.Action.Title
|
||||||
req.logEntry.ActionTitle = entities.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity)
|
req.logEntry.ActionTitle = entities.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity)
|
||||||
req.logEntry.ActionIcon = req.Binding.Action.Icon
|
req.logEntry.ActionIcon = req.Binding.Action.Icon
|
||||||
req.logEntry.ActionId = req.Binding.Action.ID
|
|
||||||
req.logEntry.Tags = req.Tags
|
req.logEntry.Tags = req.Tags
|
||||||
|
|
||||||
req.executor.logmutex.Lock()
|
req.executor.logmutex.Lock()
|
||||||
|
|
||||||
if _, containsKey := req.executor.LogsByActionId[req.Binding.Action.ID]; !containsKey {
|
if _, containsKey := req.executor.LogsByBindingId[req.Binding.ID]; !containsKey {
|
||||||
req.executor.LogsByActionId[req.Binding.Action.ID] = make([]*InternalLogEntry, 0)
|
req.executor.LogsByBindingId[req.Binding.ID] = make([]*InternalLogEntry, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.executor.LogsByActionId[req.Binding.Action.ID] = append(req.executor.LogsByActionId[req.Binding.Action.ID], req.logEntry)
|
req.executor.LogsByBindingId[req.Binding.ID] = append(req.executor.LogsByBindingId[req.Binding.ID], req.logEntry)
|
||||||
|
|
||||||
req.executor.logmutex.Unlock()
|
req.executor.logmutex.Unlock()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *Executor) FindBindingByID(id string) *ActionBinding {
|
func (e *Executor) FindBindingByID(id string) *ActionBinding {
|
||||||
e.MapActionIdToBindingLock.RLock()
|
e.MapActionBindingsLock.RLock()
|
||||||
pair, found := e.MapActionIdToBinding[id]
|
pair, found := e.MapActionBindings[id]
|
||||||
e.MapActionIdToBindingLock.RUnlock()
|
e.MapActionBindingsLock.RUnlock()
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -23,11 +23,11 @@ func (e *Executor) FindBindingByID(id string) *ActionBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) FindBindingWithNoEntity(action *config.Action) *ActionBinding {
|
func (e *Executor) FindBindingWithNoEntity(action *config.Action) *ActionBinding {
|
||||||
e.MapActionIdToBindingLock.RLock()
|
e.MapActionBindingsLock.RLock()
|
||||||
|
|
||||||
defer e.MapActionIdToBindingLock.RUnlock()
|
defer e.MapActionBindingsLock.RUnlock()
|
||||||
|
|
||||||
for _, binding := range e.MapActionIdToBinding {
|
for _, binding := range e.MapActionBindings {
|
||||||
if binding.Action == action && binding.Entity == nil {
|
if binding.Action == action && binding.Entity == nil {
|
||||||
return binding
|
return binding
|
||||||
}
|
}
|
||||||
|
|
@ -42,9 +42,9 @@ type RebuildActionMapRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) RebuildActionMap() {
|
func (e *Executor) RebuildActionMap() {
|
||||||
e.MapActionIdToBindingLock.Lock()
|
e.MapActionBindingsLock.Lock()
|
||||||
|
|
||||||
clear(e.MapActionIdToBinding)
|
clear(e.MapActionBindings)
|
||||||
|
|
||||||
req := &RebuildActionMapRequest{
|
req := &RebuildActionMapRequest{
|
||||||
Cfg: e.Cfg,
|
Cfg: e.Cfg,
|
||||||
|
|
@ -65,7 +65,7 @@ func (e *Executor) RebuildActionMap() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.MapActionIdToBindingLock.Unlock()
|
e.MapActionBindingsLock.Unlock()
|
||||||
|
|
||||||
for _, l := range e.listeners {
|
for _, l := range e.listeners {
|
||||||
l.OnActionMapRebuilt()
|
l.OnActionMapRebuilt()
|
||||||
|
|
@ -100,10 +100,10 @@ func recurseDashboardForActionTitles(component *config.DashboardComponent, req *
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAction(e *Executor, configOrder int, action *config.Action, req *RebuildActionMapRequest) {
|
func registerAction(e *Executor, configOrder int, action *config.Action, req *RebuildActionMapRequest) {
|
||||||
actionId := hashActionToID(action, "")
|
bindingId := generateActionBindingId(action, "")
|
||||||
|
|
||||||
e.MapActionIdToBinding[actionId] = &ActionBinding{
|
e.MapActionBindings[bindingId] = &ActionBinding{
|
||||||
ID: actionId,
|
ID: bindingId,
|
||||||
Action: action,
|
Action: action,
|
||||||
Entity: nil,
|
Entity: nil,
|
||||||
ConfigOrder: configOrder,
|
ConfigOrder: configOrder,
|
||||||
|
|
@ -118,9 +118,9 @@ func registerActionsFromEntities(e *Executor, configOrder int, entityTitle strin
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerActionFromEntity(e *Executor, configOrder int, tpl *config.Action, ent *entities.Entity, req *RebuildActionMapRequest) {
|
func registerActionFromEntity(e *Executor, configOrder int, tpl *config.Action, ent *entities.Entity, req *RebuildActionMapRequest) {
|
||||||
virtualActionId := hashActionToID(tpl, ent.UniqueKey)
|
virtualActionId := generateActionBindingId(tpl, ent.UniqueKey)
|
||||||
|
|
||||||
e.MapActionIdToBinding[virtualActionId] = &ActionBinding{
|
e.MapActionBindings[virtualActionId] = &ActionBinding{
|
||||||
ID: virtualActionId,
|
ID: virtualActionId,
|
||||||
Action: tpl,
|
Action: tpl,
|
||||||
Entity: ent,
|
Entity: ent,
|
||||||
|
|
@ -129,7 +129,7 @@ func registerActionFromEntity(e *Executor, configOrder int, tpl *config.Action,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashActionToID(action *config.Action, entityPrefix string) string {
|
func generateActionBindingId(action *config.Action, entityPrefix string) string {
|
||||||
if action.ID != "" && entityPrefix == "" {
|
if action.ID != "" && entityPrefix == "" {
|
||||||
return action.ID
|
return action.ID
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue