Add Pinia for state management and API data handling.

Introduced Pinia as the state management library and integrated it with the app. Implemented an `ApiClient` utility and a `dataStore` to fetch, categorize, and manage packages and stats data. Updated application entry to include Pinia and adjusted dependencies in `package.json`.
This commit is contained in:
2025-04-14 21:38:48 +02:00
parent 4e722e5e60
commit 43ce135fc6
5 changed files with 359 additions and 15 deletions

View File

@@ -11,6 +11,7 @@
"@mdi/font": "7.4.47",
"fork-awesome": "^1.2.0",
"roboto-fontface": "^0.10.0",
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vuetify": "^3.7.18"
},

View File

@@ -1,22 +1,13 @@
/**
* main.ts
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import {registerPlugins} from '@/plugins'
// Components
import { registerPlugins } from '@/plugins'
import App from './App.vue'
// Composables
import {createApp} from 'vue'
// Styles
import { createApp } from 'vue'
import '@/assets/styles/base.scss'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
registerPlugins(app)

View File

@@ -0,0 +1,258 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { dataStore } from '@/types/dataStore'
import { apiClient } from '@/utils/fetchUtils'
import { Package } from '@/types/Package'
import { Stats } from '@/types/Stats'
const UpdateInterval = Number(import.meta.env.VITE_UPDATE_INTERVAL) || 5
export const useDataStore = defineStore('data', () => {
const data = ref<dataStore>()
const loading = ref(false)
const error = ref<Error | null>(null)
let refreshInterval: NodeJS.Timeout | null = null
const init = () => {
data.value = {
lastUpdated: Date.now(),
packages: {
offset: 0,
limit: Number(import.meta.env.VITE_OFFSET) || 50,
totalPackages: [],
byStatus: {
latest: [],
built: [],
failed: [],
skipped: [],
delayed: [],
queued: [],
building: [],
signing: [],
unknown: []
},
byRepo: {
core: {
v2: [],
v3: [],
v4: []
},
extra: {
v2: [],
v3: [],
v4: []
},
multilib: {
v2: [],
v3: [],
v4: []
}
}
},
stats: {
total: 0,
built: 0,
building: 0,
latest: 0,
queued: 0,
skipped: 0,
failed: 0,
lto: {
enabled: 0,
disabled: 0,
unknown: 0
}
}
}
}
// Fetch all package data from the API
const fetchAllPackageData = async () => {
loading.value = true
error.value = null
try {
const [totalStats, totalPackages] = await Promise.allSettled([
apiClient.getStats(),
apiClient.getPackages({ limit: 0, offset: 0 })
])
// Combine into a single data structure
if (
totalPackages.status === 'fulfilled' &&
totalStats.status === 'fulfilled' &&
totalPackages.value &&
totalStats.value
) {
// Initialize if needed
if (!data.value) init()
// Update with the fetched data
data.value!.lastUpdated = Date.now()
data.value!.packages.totalPackages = [...totalPackages.value.packages]
data.value!.packages.offset = totalPackages.value.offset
data.value!.packages.limit = totalPackages.value.limit
// Update byStatus categorization
data.value!.packages.byStatus = {
latest: totalPackages.value.packages.filter((pkg) => pkg.status === 'latest') || [],
built: totalPackages.value.packages.filter((pkg) => pkg.status === 'built') || [],
failed: totalPackages.value.packages.filter((pkg) => pkg.status === 'failed') || [],
skipped: totalPackages.value.packages.filter((pkg) => pkg.status === 'skipped') || [],
delayed: totalPackages.value.packages.filter((pkg) => pkg.status === 'delayed') || [],
queued: totalPackages.value.packages.filter((pkg) => pkg.status === 'queued') || [],
building: totalPackages.value.packages.filter((pkg) => pkg.status === 'building') || [],
signing: totalPackages.value.packages.filter((pkg) => pkg.status === 'signing') || [],
unknown: totalPackages.value.packages.filter((pkg) => pkg.status === 'unknown') || []
}
// Update byRepo categorization
data.value!.packages.byRepo = {
core: {
v2: totalPackages.value.packages.filter((pkg) => pkg.repo === 'core-x86-64-v2') || [],
v3: totalPackages.value.packages.filter((pkg) => pkg.repo === 'core-x86-64-v3') || [],
v4: totalPackages.value.packages.filter((pkg) => pkg.repo === 'core-x86-64-v4') || []
},
extra: {
v2: totalPackages.value.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v2') || [],
v3: totalPackages.value.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v3') || [],
v4: totalPackages.value.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v4') || []
},
multilib: {
v2:
totalPackages.value.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v2') || [],
v3:
totalPackages.value.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v3') || [],
v4:
totalPackages.value.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v4') || []
}
}
// Update stats
data.value!.stats = {
...totalStats.value,
total: totalPackages.value.total, // Total count from API
built: data.value!.packages.byStatus.built.length,
building: data.value!.packages.byStatus.building.length
}
}
} catch (e) {
console.error('Error fetching package data:', e)
error.value = e as Error
} finally {
loading.value = false
}
}
// Fetch a specific page of packages
const fetchPackagePage = async (offset: number, limit: number) => {
loading.value = true
error.value = null
try {
const packageData = await apiClient.getPackages({ limit, offset })
if (packageData && data.value) {
data.value.packages.totalPackages = [...packageData.packages]
data.value.packages.offset = offset
data.value.packages.limit = limit
// Update byStatus categorization
data.value.packages.byStatus = {
latest: packageData.packages.filter((pkg) => pkg.status === 'latest') || [],
built: packageData.packages.filter((pkg) => pkg.status === 'built') || [],
failed: packageData.packages.filter((pkg) => pkg.status === 'failed') || [],
skipped: packageData.packages.filter((pkg) => pkg.status === 'skipped') || [],
delayed: packageData.packages.filter((pkg) => pkg.status === 'delayed') || [],
queued: packageData.packages.filter((pkg) => pkg.status === 'queued') || [],
building: packageData.packages.filter((pkg) => pkg.status === 'building') || [],
signing: packageData.packages.filter((pkg) => pkg.status === 'signing') || [],
unknown: packageData.packages.filter((pkg) => pkg.status === 'unknown') || []
}
// Update byRepo categorization
data.value.packages.byRepo = {
core: {
v2: packageData.packages.filter((pkg) => pkg.repo === 'core-x86-64-v2') || [],
v3: packageData.packages.filter((pkg) => pkg.repo === 'core-x86-64-v3') || [],
v4: packageData.packages.filter((pkg) => pkg.repo === 'core-x86-64-v4') || []
},
extra: {
v2: packageData.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v2') || [],
v3: packageData.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v3') || [],
v4: packageData.packages.filter((pkg) => pkg.repo === 'extra-x86-64-v4') || []
},
multilib: {
v2: packageData.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v2') || [],
v3: packageData.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v3') || [],
v4: packageData.packages.filter((pkg) => pkg.repo === 'multilib-x86-64-v4') || []
}
}
}
} catch (e) {
console.error('Error fetching page data:', e)
error.value = e as Error
} finally {
loading.value = false
}
}
const startAutoRefresh = async () => {
stopAutoRefresh()
init()
await fetchAllPackageData()
refreshInterval = setInterval(fetchAllPackageData, UpdateInterval * 60 * 1000)
}
const stopAutoRefresh = () => {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
}
}
const getAllPackages = (): { packages: Package[]; total: number } => {
if (!data.value) return { packages: [], total: 0 }
return {
packages: data.value.packages.totalPackages || [],
total: data.value.stats.total || 0
}
}
const getPackagesByStatus = (
status: Package['status']
): { packages: Array<Package>; total: number } => {
if (!data.value) return { packages: [], total: 0 }
return {
packages: data.value.packages.byStatus[status],
total:
status in data.value.stats
? typeof data.value.stats[status as keyof typeof data.value.stats] === 'number'
? (data.value.stats[status as keyof typeof data.value.stats] as number)
: 0
: 0
}
}
const getPackageStats = ():
| (Stats & { total: number; built: number; building: number })
| null => {
return data.value?.stats ?? null
}
return {
data,
loading,
error,
init,
fetchAllPackageData,
fetchPackagePage,
startAutoRefresh,
stopAutoRefresh,
getAllPackages,
getPackageStats,
getPackagesByStatus
}
})

View File

@@ -0,0 +1,44 @@
import { Stats } from '@/types/Stats'
import { Package } from '@/types/Package'
export interface dataStore {
lastUpdated: number
packages: {
offset: number
limit: number
totalPackages: Array<Package>
byStatus: {
latest: Array<Package>
built: Array<Package>
failed: Array<Package>
skipped: Array<Package>
delayed: Array<Package>
queued: Array<Package>
building: Array<Package>
signing: Array<Package>
unknown: Array<Package>
}
byRepo: {
core: {
v2: Array<Package>
v3: Array<Package>
v4: Array<Package>
}
extra: {
v2: Array<Package>
v3: Array<Package>
v4: Array<Package>
}
multilib: {
v2: Array<Package>
v3: Array<Package>
v4: Array<Package>
}
}
}
stats: Stats & {
total: number
built: number
building: number
}
}

View File

@@ -0,0 +1,50 @@
import { Packages } from '@/types/Packages'
import { Stats } from '@/types/Stats'
export class ApiClient {
private readonly baseUrl: string
constructor() {
this.baseUrl = import.meta.env.VITE_BASE_URL
}
async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error(`Error fetching from ${url}:`, error)
throw error // Re-throw to allow handling by caller
}
}
async getPackages(
params: {
limit?: string | number
offset?: string | number
status?: string
} = {}
): Promise<Packages> {
const { limit = 0, offset = 0, status } = params
let endpoint = `/packages?limit=${limit}&offset=${offset}`
if (status) {
endpoint += `&status=${status}`
}
return this.fetch<Packages>(endpoint)
}
async getStats(): Promise<Stats> {
return this.fetch<Stats>('/stats')
}
}
export const apiClient = new ApiClient()