mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-14 00:58:36 +00:00
feat: add GitHub repository dashboard with Prometheus metrics
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.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
faCircleDot,
|
||||
faCodePullRequest,
|
||||
faGears,
|
||||
faMagnifyingGlass,
|
||||
faTriangleExclamation,
|
||||
faXmark,
|
||||
type IconDefinition,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { ref } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { KIND_GROUPS, type Facets, type Filters, type KindGroup } from '@/lib/filters'
|
||||
import type { SeverityFilter } from '@/lib/filters'
|
||||
|
||||
defineProps<{ facets: Facets; owners: string[]; active: number }>()
|
||||
const filters = defineModel<Filters>('filters', { required: true })
|
||||
defineEmits<{ clear: [] }>()
|
||||
|
||||
const KIND_META: Record<KindGroup, { label: string; icon: IconDefinition }> = {
|
||||
workflow: { label: 'Workflows', icon: faGears },
|
||||
pr: { label: 'Pull requests', icon: faCodePullRequest },
|
||||
issue: { label: 'Issues', icon: faCircleDot },
|
||||
data: { label: 'Data', icon: faTriangleExclamation },
|
||||
}
|
||||
|
||||
const search = ref<InstanceType<typeof Input> | null>(null)
|
||||
defineExpose({ focus: () => (search.value?.$el as HTMLInputElement | undefined)?.focus() })
|
||||
|
||||
function patch(change: Partial<Filters>): void {
|
||||
filters.value = { ...filters.value, ...change }
|
||||
}
|
||||
function onSeverity(value: unknown): void {
|
||||
if (value === 'all' || value === 'crit' || value === 'warn')
|
||||
patch({ severity: value as SeverityFilter })
|
||||
else patch({ severity: 'all' })
|
||||
}
|
||||
function onKinds(value: unknown): void {
|
||||
patch({ kinds: (Array.isArray(value) ? value : []) as KindGroup[] })
|
||||
}
|
||||
function onOwners(value: unknown): void {
|
||||
patch({ owners: (Array.isArray(value) ? value : []) as string[] })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3" data-test="filter-bar">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="relative min-w-56 flex-1">
|
||||
<FontAwesomeIcon
|
||||
:icon="faMagnifyingGlass"
|
||||
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Input
|
||||
ref="search"
|
||||
:model-value="filters.query"
|
||||
type="search"
|
||||
placeholder="Search items, projects and versions"
|
||||
aria-label="Search"
|
||||
class="bg-surface pr-9 pl-9"
|
||||
data-test="search"
|
||||
@update:model-value="patch({ query: String($event ?? '') })"
|
||||
@keydown.esc="patch({ query: '' })"
|
||||
/>
|
||||
<kbd
|
||||
v-if="!filters.query"
|
||||
class="border-border text-muted-foreground pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 rounded border px-1.5 font-mono text-[11px]"
|
||||
aria-hidden="true"
|
||||
>/</kbd
|
||||
>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:model-value="filters.severity"
|
||||
aria-label="Severity"
|
||||
@update:model-value="onSeverity"
|
||||
>
|
||||
<ToggleGroupItem value="all" data-test="sev-all"
|
||||
>All {{ facets.severity.all }}</ToggleGroupItem
|
||||
>
|
||||
<ToggleGroupItem value="crit" data-test="sev-crit"
|
||||
>Critical {{ facets.severity.crit }}</ToggleGroupItem
|
||||
>
|
||||
<ToggleGroupItem value="warn" data-test="sev-warn"
|
||||
>Warnings {{ facets.severity.warn }}</ToggleGroupItem
|
||||
>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:model-value="filters.kinds"
|
||||
aria-label="Kind"
|
||||
@update:model-value="onKinds"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
v-for="kind in KIND_GROUPS"
|
||||
:key="kind"
|
||||
:value="kind"
|
||||
:disabled="facets.kinds[kind] === 0 && !filters.kinds.includes(kind)"
|
||||
:data-test="`kind-${kind}`"
|
||||
>
|
||||
<FontAwesomeIcon :icon="KIND_META[kind].icon" aria-hidden="true" />
|
||||
{{ KIND_META[kind].label }}
|
||||
<span class="text-muted-foreground tabular-nums">{{ facets.kinds[kind] }}</span>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
v-if="owners.length > 1"
|
||||
type="multiple"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:model-value="filters.owners"
|
||||
aria-label="Owner"
|
||||
@update:model-value="onOwners"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
v-for="owner in owners"
|
||||
:key="owner"
|
||||
:value="owner"
|
||||
:disabled="facets.owners[owner] === 0 && !filters.owners.includes(owner)"
|
||||
:data-test="`owner-${owner}`"
|
||||
>
|
||||
{{ owner }}
|
||||
<span class="text-muted-foreground tabular-nums">{{ facets.owners[owner] }}</span>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Button
|
||||
v-if="active > 0"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="ml-auto"
|
||||
data-test="clear-all"
|
||||
@click="$emit('clear')"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faXmark" aria-hidden="true" />
|
||||
Clear {{ active }} {{ active === 1 ? 'filter' : 'filters' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user