mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0fd6d6856 | ||
|
|
ac5eb2f16a | ||
|
|
66ed84bdbf |
@@ -155,25 +155,25 @@ func (c *Collector) Refresh(ctx context.Context) {
|
|||||||
c.limited.Store(false)
|
c.limited.Store(false)
|
||||||
c.scanDone.Store(0)
|
c.scanDone.Store(0)
|
||||||
c.scanTotal.Store(int64(len(c.names)))
|
c.scanTotal.Store(int64(len(c.names)))
|
||||||
results := make([]model.Repo, len(c.names))
|
|
||||||
sem := make(chan struct{}, c.concurrency)
|
sem := make(chan struct{}, c.concurrency)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i, name := range c.names {
|
for _, name := range c.names {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
sem <- struct{}{}
|
sem <- struct{}{}
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer func() { <-sem }()
|
defer func() { <-sem }()
|
||||||
results[i] = c.refreshOne(ctx, name, start, true)
|
r := c.refreshOne(ctx, name, start, true)
|
||||||
|
// Publish per repo so a running scan shows new and resolved alerts as they land.
|
||||||
|
c.mu.Lock()
|
||||||
|
c.repos[r.FullName] = r
|
||||||
|
c.mu.Unlock()
|
||||||
c.scanDone.Add(1)
|
c.scanDone.Add(1)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
for _, r := range results {
|
|
||||||
c.repos[r.FullName] = r
|
|
||||||
}
|
|
||||||
c.lastRefresh = start
|
c.lastRefresh = start
|
||||||
c.lastDur = c.now().Sub(start)
|
c.lastDur = c.now().Sub(start)
|
||||||
c.loaded = true
|
c.loaded = true
|
||||||
|
|||||||
@@ -553,3 +553,34 @@ func TestRefreshRepoFailureSkipsRefreshHooks(t *testing.T) {
|
|||||||
case <-time.After(200 * time.Millisecond):
|
case <-time.After(200 * time.Millisecond):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRefreshPublishesEachRepoBeforeTheScanEnds(t *testing.T) {
|
||||||
|
release := make(chan struct{})
|
||||||
|
f := &fakeFetcher{fn: func(name string) (model.Repo, error) {
|
||||||
|
if name == "o/slow" {
|
||||||
|
<-release
|
||||||
|
}
|
||||||
|
return model.Repo{FullName: name, Err: ""}, nil
|
||||||
|
}}
|
||||||
|
cfg := &config.Config{Repos: []string{"o/fast", "o/slow"}, RefreshInterval: time.Hour, Concurrency: 2, Thresholds: th()}
|
||||||
|
c := New(f, cfg, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||||
|
c.now = func() time.Time { return t0 }
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { c.Refresh(context.Background()); close(done) }()
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if v := c.View(); len(v.Repos) == 1 && v.Repos[0].FullName == "o/fast" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
v := c.View()
|
||||||
|
if len(v.Repos) != 1 || v.Repos[0].FullName != "o/fast" || !v.Refreshing || v.Loaded {
|
||||||
|
t.Fatalf("mid-scan view: repos=%d refreshing=%v loaded=%v", len(v.Repos), v.Refreshing, v.Loaded)
|
||||||
|
}
|
||||||
|
close(release)
|
||||||
|
<-done
|
||||||
|
if v := c.View(); len(v.Repos) != 2 || !v.Loaded {
|
||||||
|
t.Fatalf("after scan: repos=%d loaded=%v", len(v.Repos), v.Loaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -116,7 +116,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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]"
|
class="mt-10 grid grid-cols-[minmax(0,1fr)] gap-x-10 gap-y-10 sm:mt-14 xl:grid-cols-[minmax(0,1fr)_24rem] 2xl:grid-cols-[minmax(0,1fr)_28rem]"
|
||||||
>
|
>
|
||||||
<AttentionList
|
<AttentionList
|
||||||
ref="list"
|
ref="list"
|
||||||
@@ -133,7 +133,7 @@ onBeforeUnmount(() => {
|
|||||||
@clear="store.clearFilters()"
|
@clear="store.clearFilters()"
|
||||||
@refresh="store.refreshRepo"
|
@refresh="store.refreshRepo"
|
||||||
/>
|
/>
|
||||||
<aside class="lg:sticky lg:top-6 lg:self-start">
|
<aside class="xl:sticky xl:top-6 xl:self-start">
|
||||||
<ProjectList
|
<ProjectList
|
||||||
:repos="visibleRepos"
|
:repos="visibleRepos"
|
||||||
:total="state.repos.length"
|
:total="state.repos.length"
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const LABEL = { crit: 'Critical', warn: 'Warning', ok: 'Healthy' } as const
|
|||||||
<ul
|
<ul
|
||||||
v-else
|
v-else
|
||||||
ref="scroller"
|
ref="scroller"
|
||||||
class="border-border bg-surface divide-border max-h-[70vh] divide-y overflow-y-auto rounded-lg border [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] lg:max-h-[calc(100vh-7rem)]"
|
class="border-border bg-surface divide-border max-h-[70vh] divide-y overflow-y-auto rounded-lg border [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] xl:max-h-[calc(100vh-7rem)]"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="repo in visible"
|
v-for="repo in visible"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
import { faScrewdriverWrench, faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -44,14 +44,18 @@ function press(a: ActionDef): void {
|
|||||||
<div class="mt-2 flex flex-wrap items-center gap-2" data-test="row-actions">
|
<div class="mt-2 flex flex-wrap items-center gap-2" data-test="row-actions">
|
||||||
<template v-for="a in row.actions" :key="a.id">
|
<template v-for="a in row.actions" :key="a.id">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
size="xs"
|
class="font-semibold shadow-sm"
|
||||||
:disabled="busy(a)"
|
:disabled="busy(a)"
|
||||||
:aria-busy="busy(a)"
|
:aria-busy="busy(a)"
|
||||||
:data-test="`action-${a.id}`"
|
:data-test="`action-${a.id}`"
|
||||||
@click="press(a)"
|
@click="press(a)"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon v-if="busy(a)" :icon="faSpinner" spin aria-hidden="true" />
|
<FontAwesomeIcon
|
||||||
|
:icon="busy(a) ? faSpinner : faScrewdriverWrench"
|
||||||
|
:spin="busy(a)"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ a.label }}
|
{{ a.label }}
|
||||||
</Button>
|
</Button>
|
||||||
<Badge v-if="busy(a)" variant="secondary" data-test="action-status">Sending</Badge>
|
<Badge v-if="busy(a)" variant="secondary" data-test="action-status">Sending</Badge>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { onBeforeUnmount, onMounted, ref, type Ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref, type Ref } from 'vue'
|
||||||
|
|
||||||
/** 1 column below the breakpoint, 2 at or above it; follows the viewport live. */
|
/** 1 column below the breakpoint, 2 at or above it; follows the viewport live. */
|
||||||
export function useColumns(minWidth = '(min-width: 96rem)'): Ref<number> {
|
export function useColumns(minWidth = '(min-width: 90rem)'): Ref<number> {
|
||||||
const columns = ref(1)
|
const columns = ref(1)
|
||||||
let query: MediaQueryList | null = null
|
let query: MediaQueryList | null = null
|
||||||
const update = () => {
|
const update = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user