Added inspect task handler UI Elements
This commit is contained in:
parent
09c0267404
commit
29d30b5f54
5 changed files with 209 additions and 6 deletions
|
|
@ -437,7 +437,7 @@ const isBusy = computed<boolean>(() => loading.value || submitting.value)
|
|||
const headerTitle = computed<string>(() => props.title)
|
||||
|
||||
const guiLimitations = 'Only simple match globs, a single container selector and per-field extractors are exposed. ' +
|
||||
'More advanced constructs require editing the JSON.'
|
||||
'More advanced constructs require raw view mode.'
|
||||
|
||||
const resetGuiState = (state: GuiState): void => {
|
||||
guiState.name = state.name
|
||||
|
|
@ -471,7 +471,7 @@ const toGui = (document: TaskDefinitionDocument): GuiState | null => {
|
|||
return null
|
||||
}
|
||||
|
||||
const entry = document as Record<string, unknown>
|
||||
const entry = document
|
||||
const match = entry.match
|
||||
if (!Array.isArray(match) || match.some(item => 'string' !== typeof item)) {
|
||||
return null
|
||||
|
|
@ -614,7 +614,7 @@ const fromGui = (state: GuiState): TaskDefinitionDocument => {
|
|||
doc.request = request
|
||||
}
|
||||
|
||||
return doc as TaskDefinitionDocument
|
||||
return doc as unknown as TaskDefinitionDocument
|
||||
}
|
||||
|
||||
const parseImportedDocument = (payload: unknown): TaskDefinitionDocument => {
|
||||
|
|
@ -637,7 +637,7 @@ const parseImportedDocument = (payload: unknown): TaskDefinitionDocument => {
|
|||
}
|
||||
|
||||
const clone = JSON.parse(JSON.stringify(base)) as TaskDefinitionDocument
|
||||
const cloneRecord = clone as Record<string, unknown>
|
||||
const cloneRecord = clone
|
||||
|
||||
if ('name' in record && 'string' === typeof record.name) {
|
||||
cloneRecord.name = record.name
|
||||
|
|
|
|||
154
ui/app/components/TaskInspect.vue
Normal file
154
ui/app/components/TaskInspect.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<style scoped>
|
||||
code {
|
||||
color: var(--bulma-code) !important
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Inspect Task Handler</h2>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="field">
|
||||
<label class="label" for="url">URL <span class="has-text-danger">*</span></label>
|
||||
<div class="control">
|
||||
<input id="url" v-model="url" type="url" class="input" :class="{ 'is-danger': urlError }"
|
||||
placeholder="https://..." required />
|
||||
</div>
|
||||
<p v-if="urlError" class="help is-danger">{{ urlError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="preset">Preset</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="preset" class="is-fullwidth" v-model="preset">
|
||||
<optgroup label="Custom presets" v-if="config?.presets.filter(p => !p?.default).length > 0">
|
||||
<option v-for="cPreset in filter_presets(false)" :key="cPreset.name" :value="cPreset.name">
|
||||
{{ cPreset.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup label="Default presets">
|
||||
<option v-for="dPreset in filter_presets(true)" :key="dPreset.name" :value="dPreset.name">
|
||||
{{ dPreset.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="handler">Handler</label>
|
||||
<div class="control">
|
||||
<input id="handler" v-model="handler" type="text" class="input" placeholder="Optional handler name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit" :disabled="loading">
|
||||
<span class="icon"> <i class="fas fa-search" /></span>
|
||||
<span>Inspect</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-light" type="button" @click="onReset" :disabled="loading">
|
||||
<span class="icon"> <i class="fas fa-undo" /> </span>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="loading" class="has-text-info mt-3">
|
||||
<span class="icon">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
<span>Inspecting...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="response" class="mt-4">
|
||||
<div v-if="response.error" class="notification is-danger">
|
||||
<strong>Error:</strong> {{ response.error }}<br />
|
||||
<span v-if="response.message">{{ response.message }}</span>
|
||||
</div>
|
||||
<div class="content" v-else>
|
||||
<h4>Result:</h4>
|
||||
<pre><code>{{ response }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { request } from '~/utils'
|
||||
import { useConfigStore } from '~/stores/ConfigStore'
|
||||
import type { TaskInspectRequest, TaskInspectResponse } from '~/types/task_inspect'
|
||||
|
||||
const props = defineProps<{
|
||||
url?: string
|
||||
preset?: string
|
||||
handler?: string
|
||||
}>()
|
||||
|
||||
const config = useConfigStore()
|
||||
const url = ref<string>(props.url ?? '')
|
||||
const preset = ref<string>(props.preset || config.app.default_preset || '')
|
||||
const handler = ref<string>(props.handler ?? '')
|
||||
const loading = ref<boolean>(false)
|
||||
const response = ref<TaskInspectResponse | null>(null)
|
||||
const urlError = ref<string>('')
|
||||
|
||||
// Watch for prop changes and update fields
|
||||
watch(() => props.url, val => { if (val !== undefined) url.value = val })
|
||||
watch(() => props.preset, val => { if (val !== undefined) preset.value = val })
|
||||
watch(() => props.handler, val => { if (val !== undefined) handler.value = val })
|
||||
|
||||
const validateUrl = (val: string): boolean => {
|
||||
try {
|
||||
const u = new URL(val)
|
||||
return u.protocol === 'http:' || u.protocol === 'https:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
urlError.value = ''
|
||||
response.value = null
|
||||
|
||||
if (!url.value || !validateUrl(url.value)) {
|
||||
urlError.value = 'Please enter a valid URL.'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
const payload: TaskInspectRequest = {
|
||||
url: url.value.trim(),
|
||||
preset: preset.value.trim() || undefined,
|
||||
handler: handler.value.trim() || undefined,
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await request('/api/tasks/inspect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
response.value = await res.json()
|
||||
} catch (err: any) {
|
||||
response.value = { error: err?.message || 'Unknown error' }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
url.value = props.url || ''
|
||||
preset.value = props.preset || config.app.default_preset || ''
|
||||
handler.value = props.handler || ''
|
||||
response.value = null
|
||||
urlError.value = ''
|
||||
}
|
||||
const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
|
||||
</script>
|
||||
|
|
@ -12,13 +12,19 @@
|
|||
<div class="field is-grouped">
|
||||
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click="isEditorOpen ? closeEditor() : openCreate()"
|
||||
v-tooltip.bottom="'Toggle Form'">
|
||||
<button class="button is-primary" @click="isEditorOpen ? closeEditor() : openCreate()">
|
||||
<span class="icon"><i class="fa-solid fa-add" /></span>
|
||||
<span v-if="!isMobile">New Definition</span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button @click="() => inspect = true" class="button is-primary is-light">
|
||||
<span class="icon"><i class="fa-solid fa-magnifying-glass" /></span>
|
||||
<span v-if="!isMobile">Inspect</span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button v-tooltip.bottom="'Change display style'" class="button has-tooltip-bottom"
|
||||
@click="() => display_style = display_style === 'list' ? 'grid' : 'list'">
|
||||
|
|
@ -178,6 +184,11 @@
|
|||
@import-existing="importExistingDefinition" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal v-if="inspect" @close="() => inspect = false" :contentClass="`modal-content-max`">
|
||||
<TaskInspect />
|
||||
</Modal>
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
|
@ -237,6 +248,7 @@ const editorLoading = ref<boolean>(false)
|
|||
const editorSubmitting = ref<boolean>(false)
|
||||
const workingDefinition = ref<TaskDefinitionDocument | null>(null)
|
||||
const workingId = ref<string | null>(null)
|
||||
const inspect = ref<boolean>(false)
|
||||
const display_style = useStorage<'list' | 'grid'>('task-definitions:display', 'grid')
|
||||
|
||||
const currentSummary = computed<TaskDefinitionSummary | undefined>(() => {
|
||||
|
|
|
|||
|
|
@ -200,6 +200,11 @@
|
|||
<span>Run now</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="dropdown-item" @click="() => inspectTask = item">
|
||||
<span class="icon"><i class="fa-solid fa-magnifying-glass" /></span>
|
||||
<span>Inspect Handler</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="dropdown-divider" />
|
||||
|
||||
<NuxtLink class="dropdown-item" @click="archiveAll(item)">
|
||||
|
|
@ -324,6 +329,11 @@
|
|||
<span>Run now</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="dropdown-item" @click="() => inspectTask = item">
|
||||
<span class="icon"><i class="fa-solid fa-magnifying-glass" /></span>
|
||||
<span>Inspect Handler</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="dropdown-divider" />
|
||||
|
||||
<NuxtLink class="dropdown-item" @click="archiveAll(item)">
|
||||
|
|
@ -376,6 +386,10 @@
|
|||
<ConfirmDialog v-if="dialog_confirm.visible" :visible="dialog_confirm.visible" :title="dialog_confirm.title"
|
||||
:message="dialog_confirm.message" :options="dialog_confirm.options" @confirm="dialog_confirm.confirm"
|
||||
:html_message="dialog_confirm.html_message" @cancel="dialog_confirm = reset_dialog()" />
|
||||
|
||||
<Modal v-if="inspectTask" @close="() => inspectTask = null" :contentClass="`modal-content-max`">
|
||||
<TaskInspect :url="inspectTask.url" :preset="inspectTask.preset" />
|
||||
</Modal>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
|
@ -383,6 +397,8 @@
|
|||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
import Modal from '~/components/Modal.vue'
|
||||
import TaskInspect from '~/components/TaskInspect.vue'
|
||||
import type { task_item, exported_task, error_response } from '~/types/tasks'
|
||||
|
||||
const box = useConfirm()
|
||||
|
|
@ -405,6 +421,7 @@ const masterSelectAll = ref(false)
|
|||
const massRun = ref<boolean>(false)
|
||||
const massDelete = ref<boolean>(false)
|
||||
const table_container = ref(false)
|
||||
const inspectTask = ref<task_item | null>(null)
|
||||
|
||||
const reset_dialog = () => ({
|
||||
visible: false,
|
||||
|
|
|
|||
20
ui/app/types/task_inspect.d.ts
vendored
Normal file
20
ui/app/types/task_inspect.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Types for api/tasks/inspect
|
||||
|
||||
export interface TaskInspectRequest {
|
||||
url: string;
|
||||
preset?: string;
|
||||
handler?: string;
|
||||
}
|
||||
|
||||
export interface TaskInspectSuccess {
|
||||
// The structure depends on TaskResult, but at minimum:
|
||||
success?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface TaskInspectError {
|
||||
error: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type TaskInspectResponse = TaskInspectSuccess | TaskInspectError;
|
||||
Loading…
Reference in a new issue