mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
ghtracker authenticates as a GitHub App and scans a configured list of repositories on a schedule. It serves a dashboard, a JSON state endpoint, a health probe and Prometheus metrics. The dashboard lists only items that need attention: failing workflows, pull requests that wait too long or fail their checks, stale issues and projects that failed to scan. It has search, dynamic filters and a button that rescans on demand. The Vue app is embedded in the Go binary. The client protects the GitHub rate limit with conditional requests, a request reserve and Retry-After handling.
123 lines
4.0 KiB
Vue
123 lines
4.0 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
faCodePullRequest,
|
|
faCircleDot,
|
|
faGears,
|
|
faTriangleExclamation,
|
|
type IconDefinition,
|
|
} from '@fortawesome/free-solid-svg-icons'
|
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
import { computed } from 'vue'
|
|
import FleetStrip from '@/components/FleetStrip.vue'
|
|
import type { Repo } from '@/lib/types'
|
|
import type { Facts, RepoCounts } from '@/stores/tracker'
|
|
|
|
const props = defineProps<{
|
|
crit: number
|
|
warn: number
|
|
blockCount: number
|
|
repos: Repo[]
|
|
counts: Map<string, RepoCounts>
|
|
facts: Facts
|
|
focused: string | null
|
|
matching: Set<string>
|
|
}>()
|
|
defineEmits<{ select: [fullName: string] }>()
|
|
|
|
const rows = computed<{ label: string; value: number; icon: IconDefinition; tone: string }[]>(
|
|
() => [
|
|
{
|
|
label: 'Workflows failing',
|
|
value: props.facts.failingWorkflows,
|
|
icon: faGears,
|
|
tone: 'text-crit',
|
|
},
|
|
{
|
|
label: 'Pull requests waiting',
|
|
value: props.facts.waitingPrs,
|
|
icon: faCodePullRequest,
|
|
tone: 'text-crit',
|
|
},
|
|
{
|
|
label: 'PRs with failing checks',
|
|
value: props.facts.failingChecks,
|
|
icon: faCodePullRequest,
|
|
tone: 'text-warn',
|
|
},
|
|
{ label: 'Stale issues', value: props.facts.staleIssues, icon: faCircleDot, tone: 'text-warn' },
|
|
{
|
|
label: 'Data problems',
|
|
value: props.facts.dataProblems,
|
|
icon: faTriangleExclamation,
|
|
tone: 'text-warn',
|
|
},
|
|
],
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="grid grid-cols-[minmax(0,1fr)] items-end gap-x-14 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]"
|
|
aria-label="Fleet status"
|
|
>
|
|
<div>
|
|
<p class="text-muted-foreground mb-3 text-sm" data-test="scope">
|
|
<template v-if="blockCount > 0"
|
|
>{{ blockCount }} of {{ repos.length }} projects need attention</template
|
|
>
|
|
<template v-else>All {{ repos.length }} projects are healthy</template>
|
|
</p>
|
|
<h1
|
|
class="flex flex-wrap items-baseline gap-x-10 gap-y-1 font-semibold tracking-tight"
|
|
data-test="headline"
|
|
>
|
|
<template v-if="crit > 0 || warn > 0">
|
|
<span v-if="crit > 0" class="text-crit">
|
|
<span class="text-7xl leading-none tabular-nums">{{ crit }}</span>
|
|
<span class="ml-2 text-xl font-medium">critical</span>
|
|
</span>
|
|
<span v-if="warn > 0" class="text-warn">
|
|
<span class="text-7xl leading-none tabular-nums">{{ warn }}</span>
|
|
<span class="ml-2 text-xl font-medium">{{ warn === 1 ? 'warning' : 'warnings' }}</span>
|
|
</span>
|
|
</template>
|
|
<span v-else class="text-ok text-7xl leading-none">All clear</span>
|
|
</h1>
|
|
<div class="mt-8">
|
|
<FleetStrip
|
|
:repos="repos"
|
|
:counts="counts"
|
|
:focused="focused"
|
|
:matching="matching"
|
|
@select="$emit('select', $event)"
|
|
/>
|
|
<p class="text-muted-foreground mt-3 flex flex-wrap items-center gap-x-5 gap-y-1 text-xs">
|
|
<span class="flex items-center gap-1.5"
|
|
><span class="bg-crit size-2 rounded-[2px]"></span>critical</span
|
|
>
|
|
<span class="flex items-center gap-1.5"
|
|
><span class="bg-warn size-2 rounded-[2px]"></span>warning</span
|
|
>
|
|
<span class="flex items-center gap-1.5"
|
|
><span class="bg-ok/40 size-2 rounded-[2px]"></span>healthy</span
|
|
>
|
|
<span class="ml-auto">Select a segment to focus a project</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<dl class="border-border divide-border divide-y border-y text-sm" data-test="facts">
|
|
<div v-for="row in rows" :key="row.label" class="flex items-center gap-3 py-2.5">
|
|
<FontAwesomeIcon :icon="row.icon" class="text-muted-foreground w-4" aria-hidden="true" />
|
|
<dt class="flex-1">{{ row.label }}</dt>
|
|
<dd
|
|
class="font-mono text-base tabular-nums"
|
|
:class="row.value > 0 ? row.tone : 'text-muted-foreground'"
|
|
>
|
|
{{ row.value }}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
</template>
|