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",
|
"@mdi/font": "7.4.47",
|
||||||
"fork-awesome": "^1.2.0",
|
"fork-awesome": "^1.2.0",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
"pinia": "^3.0.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vuetify": "^3.7.18"
|
"vuetify": "^3.7.18"
|
||||||
},
|
},
|
||||||
|
@@ -1,22 +1,13 @@
|
|||||||
/**
|
import { registerPlugins } from '@/plugins'
|
||||||
* main.ts
|
|
||||||
*
|
|
||||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Plugins
|
|
||||||
import {registerPlugins} from '@/plugins'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import { createApp } from 'vue'
|
||||||
// Composables
|
|
||||||
import {createApp} from 'vue'
|
|
||||||
|
|
||||||
// Styles
|
|
||||||
import '@/assets/styles/base.scss'
|
import '@/assets/styles/base.scss'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
registerPlugins(app)
|
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