Added inspect task handler UI Elements

This commit is contained in:
arabcoders 2025-09-23 02:16:20 +03:00
parent 09c0267404
commit 29d30b5f54
5 changed files with 209 additions and 6 deletions

View file

@ -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

View 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>

View file

@ -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>(() => {

View file

@ -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
View 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;