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:
@@ -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"
|
||||
},
|
||||
|
@@ -1,22 +1,13 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
// Styles
|
||||
import '@/assets/styles/base.scss'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
|
258
frontend/src/stores/dataStore.ts
Normal file
258
frontend/src/stores/dataStore.ts
Normal 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
|
||||
}
|
||||
})
|
44
frontend/src/types/dataStore.ts
Normal file
44
frontend/src/types/dataStore.ts
Normal 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
|
||||
}
|
||||
}
|
50
frontend/src/utils/fetchUtils.ts
Normal file
50
frontend/src/utils/fetchUtils.ts
Normal 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()
|
Reference in New Issue
Block a user