mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
feat(ui): order projects by their newest problem
Projects now sort by the newest alert they hold, whether a failing workflow, a waiting pull request or a stale issue. Projects with equal ages keep the server order.
This commit is contained in:
@@ -14,7 +14,7 @@ Release archives and the image checksums are signed with cosign.
|
||||
|
||||
## What it shows
|
||||
|
||||
The dashboard lists only what needs attention, grouped by project:
|
||||
The dashboard lists only what needs attention, grouped by project. The project with the newest problem comes first:
|
||||
|
||||
- Failing workflows, including scheduled and other background ones, with a link to the run.
|
||||
- Pull requests that wait too long or fail their checks, with links and age.
|
||||
|
||||
@@ -44,18 +44,69 @@ describe('buildBlocks', () => {
|
||||
expect(buildBlocks(s)[0].rows.map((r) => r.subject)).toEqual(['crit row', 'warn row'])
|
||||
})
|
||||
|
||||
it('keeps the repo order from the server and ignores alerts for unknown repos', () => {
|
||||
it('keeps the server order for equal ages and ignores alerts for unknown repos', () => {
|
||||
const since = '2026-10-01T00:00:00Z'
|
||||
const s = state({
|
||||
repos: [repo({ fullName: 'o/b' }), repo({ fullName: 'o/a' })],
|
||||
alerts: [
|
||||
alert({ repo: 'o/a', url: '1' }),
|
||||
alert({ repo: 'o/b', url: '2' }),
|
||||
alert({ repo: 'o/ghost', url: '3' }),
|
||||
alert({ repo: 'o/a', url: '1', since }),
|
||||
alert({ repo: 'o/b', url: '2', since }),
|
||||
alert({ repo: 'o/ghost', url: '3', since }),
|
||||
],
|
||||
})
|
||||
expect(buildBlocks(s).map((b) => b.repo.fullName)).toEqual(['o/b', 'o/a'])
|
||||
})
|
||||
|
||||
it('puts the repo with the newest problem first, whatever its kind or severity', () => {
|
||||
const s = state({
|
||||
repos: [
|
||||
repo({ fullName: 'o/old-crit', health: 'crit' }),
|
||||
repo({ fullName: 'o/new-warn', health: 'warn' }),
|
||||
repo({ fullName: 'o/mid', health: 'crit' }),
|
||||
],
|
||||
alerts: [
|
||||
alert({
|
||||
repo: 'o/old-crit',
|
||||
url: 'a',
|
||||
severity: 'crit',
|
||||
kind: 'pr_waiting',
|
||||
since: '2026-05-19T00:00:00Z',
|
||||
}),
|
||||
alert({
|
||||
repo: 'o/new-warn',
|
||||
url: 'b',
|
||||
severity: 'warn',
|
||||
kind: 'workflow_stuck',
|
||||
since: '2026-10-02T12:00:00Z',
|
||||
}),
|
||||
alert({
|
||||
repo: 'o/mid',
|
||||
url: 'c',
|
||||
severity: 'crit',
|
||||
kind: 'workflow_failed',
|
||||
since: '2026-09-20T00:00:00Z',
|
||||
}),
|
||||
],
|
||||
})
|
||||
expect(buildBlocks(s).map((b) => b.repo.fullName)).toEqual([
|
||||
'o/new-warn',
|
||||
'o/mid',
|
||||
'o/old-crit',
|
||||
])
|
||||
})
|
||||
|
||||
it('ranks a repo by its newest alert, not its oldest', () => {
|
||||
const s = state({
|
||||
repos: [repo({ fullName: 'o/a', health: 'crit' }), repo({ fullName: 'o/b', health: 'crit' })],
|
||||
alerts: [
|
||||
alert({ repo: 'o/a', url: 'a1', since: '2026-01-01T00:00:00Z' }),
|
||||
alert({ repo: 'o/a', url: 'a2', since: '2026-10-02T00:00:00Z' }),
|
||||
alert({ repo: 'o/b', url: 'b1', since: '2026-06-01T00:00:00Z' }),
|
||||
],
|
||||
})
|
||||
expect(buildBlocks(s).map((b) => b.repo.fullName)).toEqual(['o/a', 'o/b'])
|
||||
})
|
||||
|
||||
it('returns no blocks when everything is healthy', () => {
|
||||
expect(buildBlocks(state({ repos: [repo()] }))).toEqual([])
|
||||
})
|
||||
|
||||
@@ -19,7 +19,9 @@ const SCAN_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
const SEVERITY_RANK = { ok: 0, warn: 1, crit: 2 } as const
|
||||
|
||||
/** Groups alerts into one row per target URL and one block per unhealthy repo. */
|
||||
const sinceMs = (since: string) => Date.parse(since) || 0
|
||||
|
||||
/** Groups alerts into one row per target URL and one block per unhealthy repo, newest problem first. */
|
||||
export function buildBlocks(state: TrackerState): Block[] {
|
||||
const rows = new Map<string, Row>()
|
||||
const order = new Map<string, string[]>()
|
||||
@@ -32,6 +34,7 @@ export function buildBlocks(state: TrackerState): Block[] {
|
||||
if (SEVERITY_RANK[alert.severity] > SEVERITY_RANK[existing.severity]) {
|
||||
existing.severity = alert.severity
|
||||
}
|
||||
if (sinceMs(alert.since) > sinceMs(existing.since)) existing.since = alert.since
|
||||
continue
|
||||
}
|
||||
rows.set(key, {
|
||||
@@ -52,7 +55,8 @@ export function buildBlocks(state: TrackerState): Block[] {
|
||||
list.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity])
|
||||
blocks.push({ repo, rows: list })
|
||||
}
|
||||
return blocks
|
||||
const newest = (b: Block) => Math.max(...b.rows.map((r) => sinceMs(r.since)))
|
||||
return blocks.sort((a, b) => newest(b) - newest(a))
|
||||
}
|
||||
|
||||
export interface RepoCounts {
|
||||
|
||||
Reference in New Issue
Block a user