1
0
mirror of https://github.com/TwiN/gatus.git synced 2026-02-04 12:56:48 +00:00

fix(suite): Display condition results when user clicks step in execution flow (#1278)

This commit is contained in:
TwiN
2025-09-19 12:43:43 -04:00
committed by GitHub
parent 1658825525
commit c87c651ff0
6 changed files with 155 additions and 28 deletions

View File

@@ -1500,6 +1500,7 @@ func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) (
// 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,15 +1515,18 @@ 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, &timestamp) err = epRows.Scan(&epResultID, &name, &success, &joinedErrors, &duration, &timestamp)
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
@@ -1532,13 +1536,52 @@ func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) (
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)
} }

View File

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

View File

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