Files
ghtracker/src/App.vue
T
lukaszraczylo a56a12a6e0 feat(ui): add a progress loading screen and lazy project list
The loading screen shows live scan progress from the server, with a pulsing outline of the dashboard. The server now reports how many repositories the running scan has finished.

The project list renders 20 rows first and adds more as the end scrolls into view. Groups on very wide screens fill two balanced columns, the fact figures sit in a row under the health strip, and the headline counts match the rows listed below it.
2026-10-02 04:50:38 +01:00

141 lines
4.1 KiB
Vue

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import AppHeader from '@/components/AppHeader.vue'
import AttentionList from '@/components/AttentionList.vue'
import LoadingScreen from '@/components/LoadingScreen.vue'
import FleetOverview from '@/components/FleetOverview.vue'
import ProjectList from '@/components/ProjectList.vue'
import { filtersFromSearch, filtersToSearch } from '@/lib/filters'
import { useTrackerStore } from '@/stores/tracker'
const POLL_MS = 60_000
const CLOCK_MS = 30_000
const store = useTrackerStore()
const {
state,
error,
notice,
scanning,
blocks,
visibleBlocks,
nowMs,
filters,
facets,
owners,
visibleRepos,
activeFilters,
repoCounts,
rowCounts,
facts,
} = storeToRefs(store)
const list = ref<InstanceType<typeof AttentionList> | null>(null)
const progress = computed(() =>
state.value && scanning.value && state.value.scan.total > 0
? `${state.value.scan.done}/${state.value.scan.total}`
: '',
)
const matching = computed(() => new Set(visibleRepos.value.map((r) => r.fullName)))
let timers: number[] = []
/** Pressing "/" outside a text field jumps to the search box. */
function onKey(e: KeyboardEvent): void {
const el = e.target as HTMLElement | null
if (e.key !== '/' || e.metaKey || e.ctrlKey || el?.closest('input, textarea, [contenteditable]'))
return
e.preventDefault()
list.value?.focusSearch()
}
// Filters live in the URL so a filtered view can be shared and survives a reload.
watch(
filters,
(f) => window.history.replaceState(null, '', window.location.pathname + filtersToSearch(f)),
{ deep: true },
)
onMounted(() => {
filters.value = filtersFromSearch(window.location.search)
window.addEventListener('keydown', onKey)
void store.load()
timers = [
window.setInterval(() => void store.load(), POLL_MS),
window.setInterval(store.advanceClock, CLOCK_MS),
]
})
onBeforeUnmount(() => {
timers.forEach((t) => window.clearInterval(t))
window.removeEventListener('keydown', onKey)
})
</script>
<template>
<main class="w-full px-4 py-4 sm:px-6 sm:py-6 lg:px-10">
<AppHeader
:last-refresh="state?.lastRefresh ?? ''"
:now-ms="nowMs"
:interval-seconds="state?.intervalSeconds ?? 0"
:loaded="state?.loaded ?? false"
:scanning="scanning"
:progress="progress"
:manual-refresh="state?.manualRefresh ?? false"
@rescan="store.rescan()"
/>
<p v-if="error" role="alert" class="text-crit mt-4 text-sm" data-test="error">
Could not reach the server: {{ error }}
</p>
<p v-if="notice" role="status" class="text-warn mt-4 text-sm" data-test="notice">
{{ notice }}
</p>
<LoadingScreen
v-if="!state || !state.loaded"
:done="state?.scan.done ?? 0"
:total="state?.scan.total ?? 0"
:connecting="!state"
/>
<template v-else>
<div class="mt-8 sm:mt-12">
<FleetOverview
:crit="rowCounts.crit"
:warn="rowCounts.warn"
:block-count="blocks.length"
:repos="state.repos"
:counts="repoCounts"
:facts="facts!"
:focused="filters.repo"
:matching="matching"
@select="store.toggleRepo"
/>
</div>
<div
class="mt-10 grid grid-cols-[minmax(0,1fr)] gap-x-10 gap-y-10 sm:mt-14 lg:grid-cols-[minmax(0,1fr)_24rem] 2xl:grid-cols-[minmax(0,1fr)_28rem]"
>
<AttentionList
ref="list"
v-model:filters="filters"
:blocks="visibleBlocks"
:now-ms="nowMs"
:crit-seconds="state.thresholds.prCritSeconds"
:facets="facets"
:owners="owners"
:active="activeFilters"
@clear="store.clearFilters()"
/>
<aside class="lg:sticky lg:top-6 lg:self-start">
<ProjectList
:repos="visibleRepos"
:total="state.repos.length"
:focused="filters.repo"
:now-ms="nowMs"
@select="store.toggleRepo"
/>
</aside>
</div>
</template>
</main>
</template>