diff --git a/frontend/package.json b/frontend/package.json index 4dff20a..a5e159e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" }, diff --git a/frontend/src/main.ts b/frontend/src/main.ts index da0f2a5..6e2eb5c 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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) diff --git a/frontend/src/stores/dataStore.ts b/frontend/src/stores/dataStore.ts new file mode 100644 index 0000000..f30fc79 --- /dev/null +++ b/frontend/src/stores/dataStore.ts @@ -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() + const loading = ref(false) + const error = ref(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; 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 + } +}) diff --git a/frontend/src/types/dataStore.ts b/frontend/src/types/dataStore.ts new file mode 100644 index 0000000..f0820e3 --- /dev/null +++ b/frontend/src/types/dataStore.ts @@ -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 + byStatus: { + latest: Array + built: Array + failed: Array + skipped: Array + delayed: Array + queued: Array + building: Array + signing: Array + unknown: Array + } + byRepo: { + core: { + v2: Array + v3: Array + v4: Array + } + extra: { + v2: Array + v3: Array + v4: Array + } + multilib: { + v2: Array + v3: Array + v4: Array + } + } + } + stats: Stats & { + total: number + built: number + building: number + } +} diff --git a/frontend/src/utils/fetchUtils.ts b/frontend/src/utils/fetchUtils.ts new file mode 100644 index 0000000..5f1d1aa --- /dev/null +++ b/frontend/src/utils/fetchUtils.ts @@ -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(endpoint: string, options: RequestInit = {}): Promise { + 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 { + const { limit = 0, offset = 0, status } = params + + let endpoint = `/packages?limit=${limit}&offset=${offset}` + if (status) { + endpoint += `&status=${status}` + } + + return this.fetch(endpoint) + } + + async getStats(): Promise { + return this.fetch('/stats') + } +} + +export const apiClient = new ApiClient()