mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-14 00:58:36 +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
|
## 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.
|
- 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.
|
- 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'])
|
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({
|
const s = state({
|
||||||
repos: [repo({ fullName: 'o/b' }), repo({ fullName: 'o/a' })],
|
repos: [repo({ fullName: 'o/b' }), repo({ fullName: 'o/a' })],
|
||||||
alerts: [
|
alerts: [
|
||||||
alert({ repo: 'o/a', url: '1' }),
|
alert({ repo: 'o/a', url: '1', since }),
|
||||||
alert({ repo: 'o/b', url: '2' }),
|
alert({ repo: 'o/b', url: '2', since }),
|
||||||
alert({ repo: 'o/ghost', url: '3' }),
|
alert({ repo: 'o/ghost', url: '3', since }),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
expect(buildBlocks(s).map((b) => b.repo.fullName)).toEqual(['o/b', 'o/a'])
|
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', () => {
|
it('returns no blocks when everything is healthy', () => {
|
||||||
expect(buildBlocks(state({ repos: [repo()] }))).toEqual([])
|
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
|
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[] {
|
export function buildBlocks(state: TrackerState): Block[] {
|
||||||
const rows = new Map<string, Row>()
|
const rows = new Map<string, Row>()
|
||||||
const order = new Map<string, string[]>()
|
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]) {
|
if (SEVERITY_RANK[alert.severity] > SEVERITY_RANK[existing.severity]) {
|
||||||
existing.severity = alert.severity
|
existing.severity = alert.severity
|
||||||
}
|
}
|
||||||
|
if (sinceMs(alert.since) > sinceMs(existing.since)) existing.since = alert.since
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rows.set(key, {
|
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])
|
list.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity])
|
||||||
blocks.push({ repo, rows: list })
|
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 {
|
export interface RepoCounts {
|
||||||
|
|||||||
Reference in New Issue
Block a user