mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-13 00:48:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1852ea0885 |
+3
-3
@@ -64,7 +64,7 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="mx-auto max-w-7xl px-5 py-6 sm:px-8">
|
<main class="w-full px-4 py-4 sm:px-6 sm:py-6 lg:px-10">
|
||||||
<AppHeader
|
<AppHeader
|
||||||
:last-refresh="state?.lastRefresh ?? ''"
|
:last-refresh="state?.lastRefresh ?? ''"
|
||||||
:now-ms="nowMs"
|
:now-ms="nowMs"
|
||||||
@@ -92,7 +92,7 @@ onBeforeUnmount(() => {
|
|||||||
Fetching data from GitHub
|
Fetching data from GitHub
|
||||||
</p>
|
</p>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="mt-12">
|
<div class="mt-8 sm:mt-12">
|
||||||
<FleetOverview
|
<FleetOverview
|
||||||
:crit="state.counts.crit"
|
:crit="state.counts.crit"
|
||||||
:warn="state.counts.warn"
|
:warn="state.counts.warn"
|
||||||
@@ -106,7 +106,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-14 grid grid-cols-[minmax(0,1fr)] gap-x-10 gap-y-12 lg:grid-cols-[minmax(0,1fr)_23rem]"
|
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
|
<AttentionList
|
||||||
ref="list"
|
ref="list"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const nextIn = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="flex items-center justify-between gap-4">
|
<header class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
class="border-border bg-surface text-link flex size-9 items-center justify-center rounded-md border"
|
class="border-border bg-surface text-link flex size-9 items-center justify-center rounded-md border"
|
||||||
@@ -37,8 +37,12 @@ const nextIn = computed(() => {
|
|||||||
</span>
|
</span>
|
||||||
<span class="text-lg font-semibold tracking-tight">ghtracker</span>
|
<span class="text-lg font-semibold tracking-tight">ghtracker</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted-foreground flex items-center gap-5 text-sm">
|
<div class="text-muted-foreground flex flex-wrap items-center gap-x-5 gap-y-1 text-sm">
|
||||||
<span v-if="updated" class="hidden sm:inline" data-test="updated">
|
<span
|
||||||
|
v-if="updated"
|
||||||
|
class="order-last w-full text-xs sm:order-none sm:w-auto sm:text-sm"
|
||||||
|
data-test="updated"
|
||||||
|
>
|
||||||
Scanned {{ updated }} ago
|
Scanned {{ updated }} ago
|
||||||
<span class="text-muted-foreground/70"> · next in {{ nextIn }}</span>
|
<span class="text-muted-foreground/70"> · next in {{ nextIn }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -52,7 +56,8 @@ const nextIn = computed(() => {
|
|||||||
@click="$emit('rescan')"
|
@click="$emit('rescan')"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon :icon="faArrowsRotate" :spin="scanning" aria-hidden="true" />
|
<FontAwesomeIcon :icon="faArrowsRotate" :spin="scanning" aria-hidden="true" />
|
||||||
{{ scanning ? 'Rescanning…' : 'Rescan repositories' }}
|
<span class="hidden sm:inline">{{ scanning ? 'Rescanning…' : 'Rescan repositories' }}</span>
|
||||||
|
<span class="sm:hidden">{{ scanning ? 'Scanning…' : 'Rescan' }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ defineExpose({ focusSearch: () => bar.value?.focus() })
|
|||||||
<FontAwesomeIcon :icon="faCircleCheck" class="text-ok text-xl" aria-hidden="true" />
|
<FontAwesomeIcon :icon="faCircleCheck" class="text-ok text-xl" aria-hidden="true" />
|
||||||
Nothing matches the current filters.
|
Nothing matches the current filters.
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-4">
|
<div v-else class="grid grid-cols-[minmax(0,1fr)] items-start gap-4 2xl:grid-cols-2">
|
||||||
<RepoGroup
|
<RepoGroup
|
||||||
v-for="block in blocks"
|
v-for="block in blocks"
|
||||||
:key="block.repo.fullName"
|
:key="block.repo.fullName"
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ const ratio = computed(() => secondsSince(props.row.since, props.nowMs) / props.
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="grid grid-cols-[2rem_minmax(0,1fr)_auto] items-start gap-x-3 py-3" data-test="row">
|
<li
|
||||||
|
class="grid grid-cols-[2rem_minmax(0,1fr)] items-start gap-x-3 gap-y-1 py-3 sm:grid-cols-[2rem_minmax(0,1fr)_auto]"
|
||||||
|
data-test="row"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="flex size-8 items-center justify-center rounded-md"
|
class="flex size-8 items-center justify-center rounded-md"
|
||||||
:class="row.severity === 'crit' ? 'bg-crit/12 text-crit' : 'bg-warn/12 text-warn'"
|
:class="row.severity === 'crit' ? 'bg-crit/12 text-crit' : 'bg-warn/12 text-warn'"
|
||||||
@@ -31,7 +34,9 @@ const ratio = computed(() => secondsSince(props.row.since, props.nowMs) / props.
|
|||||||
{{ shorten(row.detail, 120) }}
|
{{ shorten(row.detail, 120) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end gap-1.5 pt-0.5">
|
<div
|
||||||
|
class="col-start-2 flex items-center gap-3 sm:col-start-auto sm:flex-col sm:items-end sm:gap-1.5 sm:pt-0.5"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground font-mono text-xs whitespace-nowrap">
|
<span class="text-muted-foreground font-mono text-xs whitespace-nowrap">
|
||||||
{{ isZeroTime(row.since) ? '' : ageSince(row.since, nowMs) }}
|
{{ isZeroTime(row.since) ? '' : ageSince(row.since, nowMs) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function onOwners(value: unknown): void {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-3" data-test="filter-bar">
|
<div class="flex flex-col gap-3" data-test="filter-bar">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div class="relative min-w-56 flex-1">
|
<div class="relative min-w-0 basis-full sm:min-w-56 sm:flex-1 sm:basis-0">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
:icon="faMagnifyingGlass"
|
:icon="faMagnifyingGlass"
|
||||||
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
||||||
@@ -75,6 +75,7 @@ function onOwners(value: unknown): void {
|
|||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
|
class="max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="filters.severity"
|
:model-value="filters.severity"
|
||||||
@@ -96,6 +97,7 @@ function onOwners(value: unknown): void {
|
|||||||
<div class="flex flex-wrap items-center gap-x-5 gap-y-2">
|
<div class="flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="multiple"
|
type="multiple"
|
||||||
|
class="max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="filters.kinds"
|
:model-value="filters.kinds"
|
||||||
@@ -117,6 +119,7 @@ function onOwners(value: unknown): void {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
v-if="owners.length > 1"
|
v-if="owners.length > 1"
|
||||||
type="multiple"
|
type="multiple"
|
||||||
|
class="max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="filters.owners"
|
:model-value="filters.owners"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const rows = computed<{ label: string; value: number; icon: IconDefinition; tone
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="grid grid-cols-[minmax(0,1fr)] items-end gap-x-14 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]"
|
class="grid grid-cols-[minmax(0,1fr)] items-end gap-x-14 gap-y-8 lg:grid-cols-[minmax(0,1fr)_22rem]"
|
||||||
aria-label="Fleet status"
|
aria-label="Fleet status"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -68,22 +68,22 @@ const rows = computed<{ label: string; value: number; icon: IconDefinition; tone
|
|||||||
<template v-else>All {{ repos.length }} projects are healthy</template>
|
<template v-else>All {{ repos.length }} projects are healthy</template>
|
||||||
</p>
|
</p>
|
||||||
<h1
|
<h1
|
||||||
class="flex flex-wrap items-baseline gap-x-10 gap-y-1 font-semibold tracking-tight"
|
class="flex flex-wrap items-baseline gap-x-6 gap-y-1 sm:gap-x-10 font-semibold tracking-tight"
|
||||||
data-test="headline"
|
data-test="headline"
|
||||||
>
|
>
|
||||||
<template v-if="crit > 0 || warn > 0">
|
<template v-if="crit > 0 || warn > 0">
|
||||||
<span v-if="crit > 0" class="text-crit">
|
<span v-if="crit > 0" class="text-crit">
|
||||||
<span class="text-7xl leading-none tabular-nums">{{ crit }}</span>
|
<span class="text-5xl leading-none tabular-nums sm:text-7xl">{{ crit }}</span>
|
||||||
<span class="ml-2 text-xl font-medium">critical</span>
|
<span class="ml-2 text-xl font-medium">critical</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="warn > 0" class="text-warn">
|
<span v-if="warn > 0" class="text-warn">
|
||||||
<span class="text-7xl leading-none tabular-nums">{{ warn }}</span>
|
<span class="text-5xl leading-none tabular-nums sm:text-7xl">{{ warn }}</span>
|
||||||
<span class="ml-2 text-xl font-medium">{{ warn === 1 ? 'warning' : 'warnings' }}</span>
|
<span class="ml-2 text-xl font-medium">{{ warn === 1 ? 'warning' : 'warnings' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="text-ok text-7xl leading-none">All clear</span>
|
<span v-else class="text-ok text-5xl leading-none sm:text-7xl">All clear</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mt-8">
|
<div class="mt-6 sm:mt-8">
|
||||||
<FleetStrip
|
<FleetStrip
|
||||||
:repos="repos"
|
:repos="repos"
|
||||||
:counts="counts"
|
:counts="counts"
|
||||||
@@ -101,7 +101,7 @@ const rows = computed<{ label: string; value: number; icon: IconDefinition; tone
|
|||||||
<span class="flex items-center gap-1.5"
|
<span class="flex items-center gap-1.5"
|
||||||
><span class="bg-ok/40 size-2 rounded-[2px]"></span>healthy</span
|
><span class="bg-ok/40 size-2 rounded-[2px]"></span>healthy</span
|
||||||
>
|
>
|
||||||
<span class="ml-auto">Select a segment to focus a project</span>
|
<span class="sm:ml-auto">Tap a segment to focus a project</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,14 +24,19 @@ function summary(c: RepoCounts | undefined): string {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider :delay-duration="80">
|
<TooltipProvider :delay-duration="80">
|
||||||
<div class="flex gap-1" role="group" aria-label="Project health" data-test="strip">
|
<div
|
||||||
|
class="grid grid-cols-[repeat(auto-fill,minmax(1.5rem,1fr))] gap-1"
|
||||||
|
role="group"
|
||||||
|
aria-label="Project health"
|
||||||
|
data-test="strip"
|
||||||
|
>
|
||||||
<Tooltip v-for="repo in repos" :key="repo.fullName">
|
<Tooltip v-for="repo in repos" :key="repo.fullName">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="`${repo.fullName}: ${summary(counts.get(repo.fullName))}`"
|
:aria-label="`${repo.fullName}: ${summary(counts.get(repo.fullName))}`"
|
||||||
:aria-pressed="focused === repo.fullName"
|
:aria-pressed="focused === repo.fullName"
|
||||||
class="h-14 min-w-0 flex-1 rounded-[3px] transition-colors focus-visible:outline-offset-1"
|
class="h-9 min-w-0 rounded-[3px] sm:h-11 transition-colors focus-visible:outline-offset-1"
|
||||||
:class="[
|
:class="[
|
||||||
SEGMENT[repo.health],
|
SEGMENT[repo.health],
|
||||||
focused === repo.fullName
|
focused === repo.fullName
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ const LABEL = { crit: 'Critical', warn: 'Warning', ok: 'Healthy' } as const
|
|||||||
>
|
>
|
||||||
No projects match the search.
|
No projects match the search.
|
||||||
</p>
|
</p>
|
||||||
<ul v-else class="border-border bg-surface divide-border divide-y rounded-lg border">
|
<ul
|
||||||
|
v-else
|
||||||
|
class="border-border bg-surface divide-border max-h-[70vh] divide-y overflow-y-auto rounded-lg border lg:max-h-[calc(100vh-7rem)]"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="repo in repos"
|
v-for="repo in repos"
|
||||||
:key="repo.fullName"
|
:key="repo.fullName"
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ describe('AppHeader', () => {
|
|||||||
|
|
||||||
it('emits rescan and shows progress while a scan runs', async () => {
|
it('emits rescan and shows progress while a scan runs', async () => {
|
||||||
const w = mount(AppHeader, { props: { ...base, manualRefresh: true } })
|
const w = mount(AppHeader, { props: { ...base, manualRefresh: true } })
|
||||||
expect(w.get('[data-test=rescan]').text()).toBe('Rescan repositories')
|
expect(w.get('[data-test=rescan]').text()).toContain('Rescan repositories')
|
||||||
await w.get('[data-test=rescan]').trigger('click')
|
await w.get('[data-test=rescan]').trigger('click')
|
||||||
expect(w.emitted('rescan')).toHaveLength(1)
|
expect(w.emitted('rescan')).toHaveLength(1)
|
||||||
const busy = mount(AppHeader, { props: { ...base, manualRefresh: true, scanning: true } })
|
const busy = mount(AppHeader, { props: { ...base, manualRefresh: true, scanning: true } })
|
||||||
expect(busy.get('[data-test=rescan]').attributes('disabled')).toBeDefined()
|
expect(busy.get('[data-test=rescan]').attributes('disabled')).toBeDefined()
|
||||||
expect(busy.get('[data-test=rescan]').text()).toBe('Rescanning…')
|
expect(busy.get('[data-test=rescan]').text()).toContain('Rescanning…')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user