Files
ghtracker/src/components/AttentionList.vue
T
lukaszraczyloandGitHub 8cbc495dc7 feat: refresh a single repository from its own button (#2)
* feat(refresh): refresh a single repository with POST /refresh?repo=

The endpoint refreshes one configured repository and returns the full state plus the repo name.
Each repository has a 60-second cooldown that is separate from the full-refresh limit.
A running full refresh or a refresh of the same repository returns 409.
An unknown repository returns 404. Cross-site and disabled-config behaviour stay the same.
The collector reuses its fetch path and replaces only that repository.

* feat(ui): add a refresh button beside each repository

The button appears in the project list and in each attention group header.
It stays hidden when the server disables manual refresh.
The button shows a spinner while its repository refreshes and reports refusals in the notice line.
The store swaps in the returned state, so no extra fetch follows.
2026-10-02 23:16:22 +01:00

74 lines
2.4 KiB
Vue

<script setup lang="ts">
import { faCircleCheck } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref } from 'vue'
import FilterBar from '@/components/FilterBar.vue'
import RepoGroup from '@/components/RepoGroup.vue'
import { useColumns } from '@/composables/useColumns'
import { balanceColumns } from '@/lib/layout'
import type { Facets, Filters } from '@/lib/filters'
import type { Block } from '@/lib/types'
const props = defineProps<{
blocks: Block[]
nowMs: number
critSeconds: number
facets: Facets
owners: string[]
active: number
manualRefresh?: boolean
refreshing?: string[]
scanning?: boolean
}>()
const filters = defineModel<Filters>('filters', { required: true })
defineEmits<{ clear: []; refresh: [fullName: string] }>()
const columnCount = useColumns()
// A group's height is its header plus its rows, so rows+2 approximates it.
const columns = computed(() =>
balanceColumns(props.blocks, columnCount.value, (b) => b.rows.length + 2),
)
const bar = ref<InstanceType<typeof FilterBar> | null>(null)
defineExpose({ focusSearch: () => bar.value?.focus() })
</script>
<template>
<section aria-labelledby="attention-title">
<h2 id="attention-title" class="mb-4 text-lg font-semibold tracking-tight">Needs attention</h2>
<FilterBar
ref="bar"
v-model:filters="filters"
:facets="facets"
:owners="owners"
:active="active"
class="mb-5"
@clear="$emit('clear')"
/>
<div
v-if="blocks.length === 0"
class="border-border text-muted-foreground flex items-center gap-3 rounded-lg border border-dashed p-6"
data-test="empty"
>
<FontAwesomeIcon :icon="faCircleCheck" class="text-ok text-xl" aria-hidden="true" />
Nothing matches the current filters.
</div>
<div v-else class="flex items-start gap-4" data-test="columns">
<div v-for="(column, i) in columns" :key="i" class="flex min-w-0 flex-1 flex-col gap-4">
<RepoGroup
v-for="block in column"
:key="block.repo.fullName"
:block="block"
:now-ms="nowMs"
:crit-seconds="critSeconds"
:manual-refresh="manualRefresh"
:refreshing="refreshing"
:scanning="scanning"
@refresh="(name) => $emit('refresh', name)"
/>
</div>
</div>
</section>
</template>