WP-10: OS and package inventory
Debian inspector (dpkg-query, apt-get -s upgrade, snap list, os-release, reboot-required) behind a CommandRunner port with tested parsers, fake inspector for dev, persisted inventory snapshot, package_refresh job, /api/system/inventory, Updates page with OS card, summary and filterable package table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -25,7 +25,7 @@ describe('PackageTable', () => {
|
||||
expect(rows(w)).toHaveLength(1)
|
||||
await w.find('input[name=search]').setValue('')
|
||||
await w.find('input[name=upgradable]').setValue(true)
|
||||
expect(rows(w).map((r) => r.split(' ')[0])).toEqual(['openssl', 'curl'])
|
||||
expect(w.findAll('tbody tr').map((r) => r.find('td').text())).toEqual(['openssl', 'curl'])
|
||||
})
|
||||
|
||||
it('filters by source', async () => {
|
||||
|
||||
86
frontend/src/components/PackageTable.vue
Normal file
86
frontend/src/components/PackageTable.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Package } from '../api/types'
|
||||
|
||||
const props = defineProps<{ packages: Package[] }>()
|
||||
const search = ref('')
|
||||
const upgradableOnly = ref(false)
|
||||
const source = ref<'all' | 'apt' | 'snap'>('all')
|
||||
|
||||
const filtered = computed(() =>
|
||||
props.packages.filter(
|
||||
(p) =>
|
||||
(!upgradableOnly.value || p.candidate) &&
|
||||
(source.value === 'all' || p.source === source.value) &&
|
||||
p.name.toLowerCase().includes(search.value.trim().toLowerCase()),
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label for="pkg-search" class="block text-sm font-medium">Search</label>
|
||||
<input
|
||||
id="pkg-search"
|
||||
v-model="search"
|
||||
name="search"
|
||||
placeholder="package name"
|
||||
class="mt-1 rounded-md border border-gray-300 px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pkg-source" class="block text-sm font-medium">Source</label>
|
||||
<select
|
||||
id="pkg-source"
|
||||
v-model="source"
|
||||
name="source"
|
||||
class="mt-1 rounded-md border border-gray-300 px-3 py-2"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="apt">apt</option>
|
||||
<option value="snap">snap</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 pb-2 text-sm">
|
||||
<input v-model="upgradableOnly" name="upgradable" type="checkbox" />
|
||||
Upgradable only
|
||||
</label>
|
||||
<span class="pb-2 text-sm text-gray-500">{{ filtered.length }} of {{ packages.length }}</span>
|
||||
</div>
|
||||
|
||||
<table class="mt-4 w-full text-left text-sm">
|
||||
<thead class="border-b border-gray-200 text-gray-500">
|
||||
<tr>
|
||||
<th class="py-2">Package</th>
|
||||
<th>Source</th>
|
||||
<th>Installed</th>
|
||||
<th>Available</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="p in filtered"
|
||||
:key="`${p.source}:${p.name}`"
|
||||
class="border-b border-gray-100"
|
||||
:class="p.candidate ? 'bg-amber-50' : ''"
|
||||
>
|
||||
<td class="py-1.5 font-mono">{{ p.name }}</td>
|
||||
<td>{{ p.source }}</td>
|
||||
<td class="font-mono text-gray-600">{{ p.installed }}</td>
|
||||
<td class="font-mono">{{ p.candidate ?? '' }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="p.is_security"
|
||||
class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800"
|
||||
>security</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="filtered.length === 0">
|
||||
<td colspan="5" class="py-6 text-center text-gray-500">No packages match.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
Reference in New Issue
Block a user