[Execute panel] Procedures merged into actions.

Plus, a last big refactor/redesign for the panel's components.
This commit is contained in:
Fabio Manganiello 2023-10-12 02:49:51 +02:00
parent e760f8e23a
commit 0a13b4605e
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
6 changed files with 246 additions and 220 deletions

View file

@ -0,0 +1,133 @@
<template>
<div class="args-body">
<div class="args-list"
v-if="Object.keys(action.args).length || action.supportsExtraArgs">
<!-- Supported action arguments -->
<div class="arg" :key="name" v-for="name in Object.keys(action.args)">
<label>
<input type="text"
class="action-arg-value"
:class="{required: action.args[name].required}"
:disabled="running"
:placeholder="name"
:value="action.args[name].value"
@input="onArgEdit(name, $event)"
@focus="onSelect(name)">
<span class="required-flag" v-if="action.args[name].required">*</span>
</label>
<Argdoc :name="selectedArg"
:args="action.args[selectedArg]"
:doc="selectedArgdoc"
:loading="loading"
is-mobile
v-if="selectedArgdoc && selectedArg && name === selectedArg" />
</div>
<!-- Extra action arguments -->
<div class="extra-args" v-if="Object.keys(action.extraArgs).length">
<div class="arg extra-arg" :key="i" v-for="(arg, i) in action.extraArgs">
<label class="col-5">
<input type="text"
class="action-extra-arg-name"
placeholder="Name"
:disabled="running"
:value="arg.name"
@input="onExtraArgNameEdit(i, $event.target.value)">
</label>
<label class="col-6">
<input type="text"
class="action-extra-arg-value"
placeholder="Value"
:disabled="running"
:value="arg.value"
@input="onExtraArgValueEdit(i, $event.target.value)">
</label>
<label class="col-1 buttons">
<button type="button" class="action-extra-arg-del" title="Remove argument" @click="$emit('remove', i)">
<i class="fas fa-trash" />
</button>
</label>
</div>
</div>
<div class="add-arg" v-if="action.supportsExtraArgs">
<button type="button" title="Add an argument" @click="onArgAdd">
<i class="fas fa-plus" />
</button>
</div>
</div>
<Argdoc :name="selectedArg"
:args="action.args[selectedArg]"
:doc="selectedArgdoc"
:loading="loading"
v-if="selectedArgdoc && selectedArg" />
</div>
</template>
<script>
import Argdoc from "./Argdoc"
export default {
name: 'ActionArgs',
components: { Argdoc },
emits: [
'add',
'arg-edit',
'extra-arg-name-edit',
'extra-arg-value-edit',
'remove',
'select',
],
props: {
action: Object,
loading: Boolean,
running: Boolean,
selectedArg: String,
selectedArgdoc: String,
},
methods: {
onArgAdd() {
this.$emit('add')
this.$nextTick(() => {
const args = this.$el.querySelectorAll('.action-extra-arg-name')
if (!args.length)
return
args[args.length - 1].focus()
})
},
onArgEdit(name, event) {
this.$emit('arg-edit', {
name: name,
value: event.target.value,
})
},
onExtraArgNameEdit(i, value) {
this.$emit('extra-arg-name-edit', {
index: i,
value: value,
})
},
onExtraArgValueEdit(i, value) {
this.$emit('extra-arg-value-edit', {
index: i,
value: value,
})
},
onSelect(arg) {
this.$emit('select', arg)
},
},
}
</script>
<style lang="scss" scoped>
@import "common";
</style>

View file

@ -0,0 +1,40 @@
<template>
<section class="doc-container" v-if="doc?.length">
<h2>
<div class="title">
<i class="fas fa-book" /> &nbsp;
<a :href="action?.doc_url">Action documentation</a>
</div>
<div class="buttons" v-if="action?.name">
<button type="button" title="cURL command" v-if="curlSnippet?.length" @click="$emit('curl-modal')">
<i class="fas fa-terminal" />
</button>
</div>
</h2>
<div class="doc html">
<Loading v-if="loading" />
<span v-html="doc" v-else />
</div>
</section>
</template>
<script>
import Loading from "@/components/Loading"
export default {
name: 'ActionDoc',
components: { Loading },
emits: ['curl-modal'],
props: {
action: Object,
doc: String,
curlSnippet: String,
loading: Boolean,
}
}
</script>
<style lang="scss" scoped>
@import "common";
</style>

View file

@ -49,25 +49,12 @@
</header> </header>
<!-- Action documentation container --> <!-- Action documentation container -->
<section class="doc-container" v-if="selectedDoc"> <ActionDoc
<h2> :action="action"
<div class="title"> :curl-snippet="curlSnippet"
<i class="fas fa-book" /> &nbsp; :loading="docLoading"
<a :href="this.action?.doc_url">Action documentation</a> :doc="selectedDoc"
</div> @curl-modal="$refs.curlModal.show()" />
<div class="buttons" v-if="action?.name">
<button type="button" title="cURL command" v-if="curlSnippet?.length"
@click="$refs.curlModal.show()">
<i class="fas fa-terminal" />
</button>
</div>
</h2>
<div class="doc html">
<Loading v-if="docLoading" />
<span v-html="selectedDoc" v-else />
</div>
</section>
<!-- Action arguments container --> <!-- Action arguments container -->
<section class="args" <section class="args"
@ -77,66 +64,20 @@
Arguments Arguments
</h2> </h2>
<div class="args-body"> <ActionArgs :action="action"
<div class="args-list" :loading="loading"
v-if="Object.keys(action.args).length || action.supportsExtraArgs"> :running="running"
<!-- Supported action arguments --> :selected-arg="selectedArg"
<div class="arg" :key="name" v-for="name in Object.keys(action.args)"> :selected-argdoc="selectedArgdoc"
<label> @add="addArg"
<input @select="selectArgdoc"
type="text" @remove="removeArg"
class="action-arg-value" @arg-edit="action.args[$event.name].value = $event.value"
:class="{required: action.args[name].required}" @extra-arg-name-edit="action.extraArgs[$event.index].name = $event.value"
:disabled="running" @extra-arg-value-edit="action.extraArgs[$event.index].value = $event.value" />
:placeholder="name"
v-model="action.args[name].value"
@focus="selectArgdoc(name)">
<span class="required-flag" v-if="action.args[name].required">*</span>
</label>
<Argdoc :name="selectedArg"
:args="action.args[selectedArg]"
:doc="selectedArgdoc"
:loading="docLoading"
is-mobile
v-if="selectedArgdoc && selectedArg && name === selectedArg" />
</div>
<!-- Extra action arguments -->
<div class="extra-args" v-if="Object.keys(action.extraArgs).length">
<div class="arg extra-arg" :key="i" v-for="i in Object.keys(action.extraArgs)">
<label class="col-5">
<input type="text" class="action-extra-arg-name" :disabled="running"
placeholder="Name" v-model="action.extraArgs[i].name">
</label>
<label class="col-6">
<input type="text" class="action-extra-arg-value" :disabled="running"
placeholder="Value" v-model="action.extraArgs[i].value">
</label>
<label class="col-1 buttons">
<button type="button" class="action-extra-arg-del" title="Remove argument"
@click="removeArg(i)">
<i class="fas fa-trash" />
</button>
</label>
</div>
</div>
<div class="add-arg" v-if="action.supportsExtraArgs">
<button type="button" title="Add an argument" @click="addArg">
<i class="fas fa-plus" />
</button>
</div>
</div>
<Argdoc :name="selectedArg"
:args="action.args[selectedArg]"
:doc="selectedArgdoc"
:loading="docLoading"
v-if="selectedArgdoc && selectedArg" />
</div>
</section> </section>
<!-- Structured response container -->
<Response :response="response" :error="error" /> <Response :response="response" :error="error" />
</div> </div>
@ -151,45 +92,17 @@
</button> </button>
</div> </div>
<!-- Raw response container -->
<Response :response="response" :error="error" /> <Response :response="response" :error="error" />
</div> </div>
</form> </form>
</main> </main>
<!-- Procedures section (to be removed) -->
<div class="section procedures-container" v-if="Object.keys(procedures).length">
<h1>Execute Procedure</h1>
<div class="procedure" :class="selectedProcedure.name === name ? 'selected' : ''"
v-for="name in Object.keys(procedures).sort()" :key="name" @click="updateProcedure(name, $event)">
<form ref="procedureForm" autocomplete="off" @submit.prevent="executeProcedure">
<div class="head">
<div class="name col-no-margin-11" v-text="name" />
<div class="btn-container col-no-margin-1">
<button type="submit" class="run-btn btn-default" :disabled="running" title="Run"
@click.stop="$emit('submit')" v-if="selectedProcedure.name === name">
<i class="fas fa-play" />
</button>
</div>
</div>
<div class="args-list" v-if="selectedProcedure.name === name">
<div class="arg"
v-for="argname in Object.keys(selectedProcedure.args)"
:key="argname">
<label>
<input type="text" class="action-arg-value" @click="$event.stopPropagation()" :disabled="running"
:placeholder="argname" v-model="selectedProcedure.args[argname]">
</label>
</div>
</div>
</form>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import Argdoc from "./Argdoc" import ActionArgs from "./ActionArgs"
import ActionDoc from "./ActionDoc"
import Autocomplete from "@/components/elements/Autocomplete" import Autocomplete from "@/components/elements/Autocomplete"
import Loading from "@/components/Loading" import Loading from "@/components/Loading"
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
@ -200,7 +113,16 @@ import Utils from "@/Utils"
export default { export default {
name: "Execute", name: "Execute",
components: {Argdoc, Autocomplete, Loading, Modal, Response, Tab, Tabs}, components: {
ActionArgs,
ActionDoc,
Autocomplete,
Loading,
Modal,
Response,
Tab,
Tabs,
},
mixins: [Utils], mixins: [Utils],
data() { data() {
@ -212,11 +134,6 @@ export default {
selectedDoc: undefined, selectedDoc: undefined,
selectedArg: undefined, selectedArg: undefined,
selectedArgdoc: undefined, selectedArgdoc: undefined,
selectedProcedure: {
name: undefined,
args: {},
},
response: undefined, response: undefined,
error: undefined, error: undefined,
rawRequest: undefined, rawRequest: undefined,
@ -273,7 +190,7 @@ export default {
}, {}), }, {}),
...this.action.extraArgs.reduce((args, arg) => { ...this.action.extraArgs.reduce((args, arg) => {
let value = args[arg.value] let value = arg.value
try { try {
value = JSON.parse(value) value = JSON.parse(value)
} catch (e) { } catch (e) {
@ -301,11 +218,16 @@ export default {
args: this.requestArgs, args: this.requestArgs,
} }
const reqStr = JSON.stringify(request, null, 2)
return ( return (
'curl -XPOST -H "Content-Type: application/json" \\\n\t' + 'curl -XPOST -H "Content-Type: application/json" \\\n ' +
`-H "Cookie: session_token=${this.getCookies()['session_token']}"`+ `-H "Cookie: session_token=${this.getCookies()['session_token']}"`+
" \\\n\t -d '" + " \\\n -d '\n {\n " +
this.indent(JSON.stringify(request, null, 2), 2).trim() + "' \\\n\t" + this.indent(
reqStr.split('\n').slice(1, reqStr.length - 2).join('\n'), 2
).trim() +
"' \\\n " +
`'${this.curlURL}'` `'${this.curlURL}'`
) )
}, },
@ -316,12 +238,36 @@ export default {
this.loading = true this.loading = true
try { try {
this.procedures = await this.request('inspect.get_procedures') [this.procedures, this.plugins] = await Promise.all([
this.plugins = await this.request('inspect.get_all_plugins') this.request('inspect.get_procedures'),
this.request('inspect.get_all_plugins'),
])
} finally { } finally {
this.loading = false this.loading = false
} }
// Register procedures as actions
this.plugins.procedure = {
name: 'procedure',
actions: Object.entries(this.procedures || {}).reduce((actions, [name, procedure]) => {
actions[name] = {
name: name,
args: (procedure.args || []).reduce((args, arg) => {
args[arg] = {
name: arg,
required: false,
}
return args
}, {}),
supportsExtraArgs: true,
}
return actions
}, {}),
}
// Parse actions from the plugins map
for (const plugin of Object.values(this.plugins)) { for (const plugin of Object.values(this.plugins)) {
for (const action of Object.values(plugin.actions)) { for (const action of Object.values(plugin.actions)) {
action.name = plugin.name + '.' + action.name action.name = plugin.name + '.' + action.name
@ -380,34 +326,6 @@ export default {
return await this.request('utils.rst_to_html', {text: docString}) return await this.request('utils.rst_to_html', {text: docString})
}, },
updateProcedure(name, event) {
if (event.target.getAttribute('type') === 'submit') {
return
}
if (this.selectedProcedure.name === name) {
this.selectedProcedure = {
name: undefined,
args: {},
}
return
}
if (!(name in this.procedures)) {
console.warn('Procedure not found: ' + name)
return
}
this.selectedProcedure = {
name: name,
args: (this.procedures[name].args || []).reduce((args, arg) => {
args[arg] = undefined
return args
}, {})
}
},
addArg() { addArg() {
this.action.extraArgs.push({ this.action.extraArgs.push({
name: undefined, name: undefined,
@ -494,33 +412,6 @@ export default {
} }
}, },
executeProcedure(event) {
if (!this.selectedProcedure.name || this.running)
return
event.stopPropagation()
this.running = true
const args = {
...Object.entries(this.selectedProcedure.args).reduce((args, arg) => {
if (arg[1] != null) {
let value = arg[1]
try {
value = JSON.parse(value)
} catch (e) {
console.debug('Not a valid JSON value')
console.debug(value)
}
args[arg[0]] = value
}
return args
}, {}),
}
this.request('procedure.' + this.selectedProcedure.name, args)
.then(this.onResponse).catch(this.onError).finally(this.onDone)
},
onClick(event) { onClick(event) {
// Intercept any clicks from RST rendered links and open them in a new tab // Intercept any clicks from RST rendered links and open them in a new tab
if (event.target.tagName.toLowerCase() === 'a') { if (event.target.tagName.toLowerCase() === 'a') {
@ -569,48 +460,6 @@ export default {
margin: 0 .5em; margin: 0 .5em;
} }
.procedures-container {
.procedure {
background: $background-color;
border-bottom: $default-border-2;
padding: 1.5em .5em;
cursor: pointer;
&:hover {
background: $hover-bg;
}
&.selected {
background: $selected-bg;
}
form {
background: none;
display: flex;
margin-bottom: 0 !important;
flex-direction: column;
box-shadow: none;
}
.head {
display: flex;
align-items: center;
}
.btn-container {
text-align: right;
}
button {
background: $procedure-submit-btn-bg;
}
}
.action-arg-value {
margin: 0.25em 0;
}
}
.run-btn { .run-btn {
background: $background-color; background: $background-color;
border-radius: .25em; border-radius: .25em;

View file

@ -21,8 +21,11 @@
</template> </template>
<script> <script>
import Utils from "@/Utils"
export default { export default {
name: 'Response', name: 'Response',
mixins: [Utils],
props: { props: {
response: String, response: String,
error: String, error: String,

View file

@ -19,7 +19,7 @@ h1 {
padding: .75em .5em; padding: .75em .5em;
box-shadow: $title-shadow; box-shadow: $title-shadow;
font-size: 1.1em; font-size: 1.1em;
margin-bottom: 0 !important; margin: 0;
@include from($desktop) { @include from($desktop) {
border-radius: 0.5em 0.5em 0 0; border-radius: 0.5em 0.5em 0 0;
@ -246,6 +246,7 @@ textarea.curl-snippet {
height: 100vh; height: 100vh;
max-width: 40em; max-width: 40em;
max-height: 25em; max-height: 25em;
font-family: monospace;
line-break: anywhere; line-break: anywhere;
overflow: auto; overflow: auto;
padding: 0.5em; padding: 0.5em;

View file

@ -13,8 +13,8 @@ export default {
return text.split('_').map((t) => this.capitalize(t)).join(' ') return text.split('_').map((t) => this.capitalize(t)).join(' ')
}, },
indent(text, tabs = 1) { indent(text, spaces = 2) {
return text.split('\n').map((t) => `${'\t'.repeat(tabs)}${t}`).join('\n') return text.split('\n').map((t) => `${' '.repeat(spaces)}${t}`).join('\n')
}, },
}, },
} }