mirror of
https://github.com/TwiN/gatus.git
synced 2026-02-04 11:11:44 +00:00
fix(suite): Display condition results when user clicks step in execution flow (#1278)
This commit is contained in:
@@ -1499,7 +1499,8 @@ func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) (
|
|||||||
resultID := data.id
|
resultID := data.id
|
||||||
// Query endpoint results for this suite result
|
// Query endpoint results for this suite result
|
||||||
epRows, err := tx.Query(`
|
epRows, err := tx.Query(`
|
||||||
SELECT
|
SELECT
|
||||||
|
er.endpoint_result_id,
|
||||||
e.endpoint_name,
|
e.endpoint_name,
|
||||||
er.success,
|
er.success,
|
||||||
er.errors,
|
er.errors,
|
||||||
@@ -1514,31 +1515,73 @@ func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) (
|
|||||||
logr.Errorf("[sql.getSuiteResults] Failed to get endpoint results for suite_result_id=%d: %s", resultID, err.Error())
|
logr.Errorf("[sql.getSuiteResults] Failed to get endpoint results for suite_result_id=%d: %s", resultID, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Map to store endpoint results by their ID for condition lookup
|
||||||
|
epResultMap := make(map[int64]*endpoint.Result)
|
||||||
epCount := 0
|
epCount := 0
|
||||||
for epRows.Next() {
|
for epRows.Next() {
|
||||||
epCount++
|
epCount++
|
||||||
|
var epResultID int64
|
||||||
var name string
|
var name string
|
||||||
var success bool
|
var success bool
|
||||||
var joinedErrors string
|
var joinedErrors string
|
||||||
var duration int64
|
var duration int64
|
||||||
var timestamp time.Time
|
var timestamp time.Time
|
||||||
err = epRows.Scan(&name, &success, &joinedErrors, &duration, ×tamp)
|
err = epRows.Scan(&epResultID, &name, &success, &joinedErrors, &duration, ×tamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logr.Errorf("[sql.getSuiteResults] Failed to scan endpoint result: %s", err.Error())
|
logr.Errorf("[sql.getSuiteResults] Failed to scan endpoint result: %s", err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
epResult := &endpoint.Result{
|
epResult := &endpoint.Result{
|
||||||
Name: name,
|
Name: name,
|
||||||
Success: success,
|
Success: success,
|
||||||
Duration: time.Duration(duration),
|
Duration: time.Duration(duration),
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{}, // Initialize empty slice
|
||||||
}
|
}
|
||||||
if len(joinedErrors) > 0 {
|
if len(joinedErrors) > 0 {
|
||||||
epResult.Errors = strings.Split(joinedErrors, arraySeparator)
|
epResult.Errors = strings.Split(joinedErrors, arraySeparator)
|
||||||
}
|
}
|
||||||
|
epResultMap[epResultID] = epResult
|
||||||
result.EndpointResults = append(result.EndpointResults, epResult)
|
result.EndpointResults = append(result.EndpointResults, epResult)
|
||||||
}
|
}
|
||||||
epRows.Close()
|
epRows.Close()
|
||||||
|
// Fetch condition results for all endpoint results in this suite result
|
||||||
|
if len(epResultMap) > 0 {
|
||||||
|
args := make([]interface{}, 0, len(epResultMap))
|
||||||
|
condQuery := `SELECT endpoint_result_id, condition, success
|
||||||
|
FROM endpoint_result_conditions
|
||||||
|
WHERE endpoint_result_id IN (`
|
||||||
|
index := 1
|
||||||
|
for epResultID := range epResultMap {
|
||||||
|
condQuery += "$" + strconv.Itoa(index) + ","
|
||||||
|
args = append(args, epResultID)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
condQuery = condQuery[:len(condQuery)-1] + ")"
|
||||||
|
|
||||||
|
condRows, err := tx.Query(condQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
logr.Errorf("[sql.getSuiteResults] Failed to get condition results for suite_result_id=%d: %s", resultID, err.Error())
|
||||||
|
} else {
|
||||||
|
condCount := 0
|
||||||
|
for condRows.Next() {
|
||||||
|
condCount++
|
||||||
|
conditionResult := &endpoint.ConditionResult{}
|
||||||
|
var epResultID int64
|
||||||
|
if err = condRows.Scan(&epResultID, &conditionResult.Condition, &conditionResult.Success); err != nil {
|
||||||
|
logr.Errorf("[sql.getSuiteResults] Failed to scan condition result: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if epResult, exists := epResultMap[epResultID]; exists {
|
||||||
|
epResult.ConditionResults = append(epResult.ConditionResults, conditionResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
condRows.Close()
|
||||||
|
if condCount > 0 {
|
||||||
|
logr.Debugf("[sql.getSuiteResults] Found %d condition results for suite_result_id=%d", condCount, resultID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if epCount > 0 {
|
if epCount > 0 {
|
||||||
logr.Debugf("[sql.getSuiteResults] Found %d endpoint results for suite_result_id=%d", epCount, resultID)
|
logr.Debugf("[sql.getSuiteResults] Found %d endpoint results for suite_result_id=%d", epCount, resultID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Condition Results -->
|
||||||
|
<div v-if="step.result?.conditionResults?.length" class="space-y-2">
|
||||||
|
<h3 class="text-sm font-medium flex items-center gap-2">
|
||||||
|
<CheckCircle class="w-4 h-4" />
|
||||||
|
Condition Results ({{ step.result.conditionResults.length }})
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 max-h-48 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="(conditionResult, index) in step.result.conditionResults"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-start gap-3 p-1 rounded-lg border"
|
||||||
|
:class="conditionResult.success
|
||||||
|
? 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-700'
|
||||||
|
: 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-700'"
|
||||||
|
>
|
||||||
|
<!-- Status icon -->
|
||||||
|
<div class="flex-shrink-0 mt-0.5">
|
||||||
|
<CheckCircle
|
||||||
|
v-if="conditionResult.success"
|
||||||
|
class="w-4 h-4 text-green-600 dark:text-green-400"
|
||||||
|
/>
|
||||||
|
<XCircle
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4 text-red-600 dark:text-red-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Condition text -->
|
||||||
|
<div class="flex-1 min-w-0 flex items-center justify-between gap-3">
|
||||||
|
<p class="text-sm font-mono break-all"
|
||||||
|
:class="conditionResult.success
|
||||||
|
? 'text-green-800 dark:text-green-200'
|
||||||
|
: 'text-red-800 dark:text-red-200'">
|
||||||
|
{{ conditionResult.condition }}
|
||||||
|
</p>
|
||||||
|
<span class="text-xs font-medium whitespace-nowrap"
|
||||||
|
:class="conditionResult.success
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-red-600 dark:text-red-400'">
|
||||||
|
{{ conditionResult.success ? 'Passed' : 'Failed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Endpoint Configuration -->
|
||||||
|
<div v-if="step.endpoint" class="space-y-2">
|
||||||
|
<h3 class="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Settings class="w-4 h-4" />
|
||||||
|
Endpoint Configuration
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 text-xs">
|
||||||
|
<div v-if="step.endpoint.url">
|
||||||
|
<span class="text-muted-foreground">URL:</span>
|
||||||
|
<p class="font-mono mt-1 break-all">{{ step.endpoint.url }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="step.endpoint.method">
|
||||||
|
<span class="text-muted-foreground">Method:</span>
|
||||||
|
<p class="mt-1 font-medium">{{ step.endpoint.method }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="step.endpoint.interval">
|
||||||
|
<span class="text-muted-foreground">Interval:</span>
|
||||||
|
<p class="mt-1">{{ step.endpoint.interval }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="step.endpoint.timeout">
|
||||||
|
<span class="text-muted-foreground">Timeout:</span>
|
||||||
|
<p class="mt-1">{{ step.endpoint.timeout }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result Errors (separate from step errors) -->
|
||||||
|
<div v-if="step.result?.errors?.length" class="space-y-2">
|
||||||
|
<h3 class="text-sm font-medium flex items-center gap-2 text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle class="w-4 h-4" />
|
||||||
|
Result Errors ({{ step.result.errors.length }})
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 max-h-32 overflow-y-auto">
|
||||||
|
<div v-for="(error, index) in step.result.errors" :key="index"
|
||||||
|
class="p-3 bg-red-50 dark:bg-red-900/50 border border-red-200 dark:border-red-700 rounded text-sm font-mono text-red-800 dark:text-red-300 break-all">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +167,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { X, AlertCircle, RotateCcw, Download, CheckCircle, XCircle, SkipForward, Pause, Clock } from 'lucide-vue-next'
|
import { X, AlertCircle, RotateCcw, Download, CheckCircle, XCircle, SkipForward, Pause, Clock, Settings } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { formatDuration } from '@/utils/format'
|
import { formatDuration } from '@/utils/format'
|
||||||
import { prettifyTimestamp } from '@/utils/time'
|
import { prettifyTimestamp } from '@/utils/time'
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<!-- Enhanced Execution Flow -->
|
<!-- Enhanced Execution Flow -->
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<h3 class="text-lg font-semibold mb-4">Execution Flow</h3>
|
<h3 class="text-lg font-semibold mb-4">Execution Flow</h3>
|
||||||
<SequentialFlowDiagram
|
<SequentialFlowDiagram
|
||||||
:flow-steps="flowSteps"
|
:flow-steps="flowSteps"
|
||||||
:progress-percentage="executionProgress"
|
:progress-percentage="executionProgress"
|
||||||
:completed-steps="completedStepsCount"
|
:completed-steps="completedStepsCount"
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<Settings @refreshData="fetchData" />
|
<Settings @refreshData="fetchData" />
|
||||||
|
|
||||||
<!-- Step Details Modal -->
|
<!-- Step Details Modal -->
|
||||||
<StepDetailsModal
|
<StepDetailsModal
|
||||||
v-if="selectedStep"
|
v-if="selectedStep"
|
||||||
:step="selectedStep"
|
:step="selectedStep"
|
||||||
:index="selectedStepIndex"
|
:index="selectedStepIndex"
|
||||||
@@ -255,13 +255,10 @@ const flowSteps = computed(() => {
|
|||||||
if (!latestResult.value || !latestResult.value.endpointResults) {
|
if (!latestResult.value || !latestResult.value.endpointResults) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = latestResult.value.endpointResults
|
const results = latestResult.value.endpointResults
|
||||||
|
|
||||||
return results.map((result, index) => {
|
return results.map((result, index) => {
|
||||||
const endpoint = suite.value?.endpoints?.[index]
|
const endpoint = suite.value?.endpoints?.[index]
|
||||||
const nextResult = results[index + 1]
|
const nextResult = results[index + 1]
|
||||||
|
|
||||||
// Determine if this is an always-run endpoint by checking execution pattern
|
// Determine if this is an always-run endpoint by checking execution pattern
|
||||||
// If a previous step failed but this one still executed, it must be always-run
|
// If a previous step failed but this one still executed, it must be always-run
|
||||||
let isAlwaysRun = false
|
let isAlwaysRun = false
|
||||||
@@ -272,7 +269,6 @@ const flowSteps = computed(() => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: endpoint?.name || result.name || `Step ${index + 1}`,
|
name: endpoint?.name || result.name || `Step ${index + 1}`,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
@@ -296,21 +292,17 @@ const executionProgress = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const determineStepStatus = (result) => {
|
const determineStepStatus = (result) => {
|
||||||
if (!result) return 'not-started'
|
if (!result) return 'not-started'
|
||||||
|
|
||||||
// Check if step was skipped
|
// Check if step was skipped
|
||||||
if (result.conditionResults && result.conditionResults.some(c => c.condition.includes('SKIP'))) {
|
if (result.conditionResults && result.conditionResults.some(c => c.condition.includes('SKIP'))) {
|
||||||
return 'skipped'
|
return 'skipped'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if step failed but is always-run (still shows as failed but executed)
|
// Check if step failed but is always-run (still shows as failed but executed)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return 'failed'
|
return 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user