Compare commits
10 Commits
4ffb4b2855
...
4d564c1d11
Author | SHA1 | Date | |
---|---|---|---|
4d564c1d11 | |||
2af2e83164 | |||
f6af87dc27 | |||
114a34de8f | |||
f8c46796ae | |||
68d88469c1 | |||
9bb1e2add4 | |||
617df25581 | |||
80d98cade6 | |||
0c81399ea1 |
@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
AURCache is a build server and repository for Archlinux packages sourced from the AUR (Arch User Repository). It features a Flutter frontend and Rust backend, enabling users to add packages for building and subsequently serves them as a pacman repository. Notably, AURCache automatically detects when a package is out of date and displays it within the frontend.
|
AURCache is a build server and repository for Archlinux packages sourced from the AUR (Arch User Repository). It features a Flutter frontend and Rust backend, enabling users to add packages for building and subsequently serves them as a pacman repository. Notably, AURCache automatically detects when a package is out of date and displays it within the frontend.
|
||||||
|
|
||||||
|
<p><img src="res/imgs/screenshot1.png" alt=""/>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>More Images:</summary>
|
||||||
|
<br>
|
||||||
|
<img src="res/imgs/screenshot2.png" alt=""/>
|
||||||
|
<img src="res/imgs/screenshot3.png" alt=""/></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
## Deployment with Docker and Docker-compose
|
## Deployment with Docker and Docker-compose
|
||||||
|
|
||||||
To deploy AURCache using Docker and Docker-compose, you can use the following example docker-compose.yml file:
|
To deploy AURCache using Docker and Docker-compose, you can use the following example docker-compose.yml file:
|
||||||
|
@ -19,6 +19,7 @@ pub fn build_api() -> Vec<Route> {
|
|||||||
stats,
|
stats,
|
||||||
get_build,
|
get_build,
|
||||||
get_package,
|
get_package,
|
||||||
package_update_endpoint
|
package_update_endpoint,
|
||||||
|
cancel_build
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use crate::db::prelude::Builds;
|
|||||||
use crate::db::{builds, packages, versions};
|
use crate::db::{builds, packages, versions};
|
||||||
use rocket::response::status::NotFound;
|
use rocket::response::status::NotFound;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{delete, get, State};
|
use rocket::{delete, get, post, State};
|
||||||
|
|
||||||
use crate::api::types::input::ListBuildsModel;
|
use crate::api::types::input::ListBuildsModel;
|
||||||
use rocket_okapi::openapi;
|
use rocket_okapi::openapi;
|
||||||
@ -12,6 +12,8 @@ use sea_orm::QueryFilter;
|
|||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
DatabaseConnection, EntityTrait, ModelTrait, QueryOrder, QuerySelect, RelationTrait,
|
DatabaseConnection, EntityTrait, ModelTrait, QueryOrder, QuerySelect, RelationTrait,
|
||||||
};
|
};
|
||||||
|
use tokio::sync::broadcast::Sender;
|
||||||
|
use crate::builder::types::Action;
|
||||||
|
|
||||||
#[openapi(tag = "build")]
|
#[openapi(tag = "build")]
|
||||||
#[get("/build/<buildid>/output?<startline>")]
|
#[get("/build/<buildid>/output?<startline>")]
|
||||||
@ -142,3 +144,17 @@ pub async fn delete_build(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "build")]
|
||||||
|
#[post("/build/<buildid>/cancel")]
|
||||||
|
pub async fn cancel_build(
|
||||||
|
db: &State<DatabaseConnection>,
|
||||||
|
tx: &State<Sender<Action>>,
|
||||||
|
buildid: i32,
|
||||||
|
) -> Result<(), NotFound<String>> {
|
||||||
|
let db = db as &DatabaseConnection;
|
||||||
|
|
||||||
|
let _ = tx.send(Action::Cancel(buildid)).map_err(|e| NotFound(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -53,7 +53,7 @@ async fn get_stats(db: &DatabaseConnection) -> anyhow::Result<ListStats> {
|
|||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
struct BuildTimeStruct {
|
struct BuildTimeStruct {
|
||||||
avg_build_time: f64,
|
avg_build_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let unique: BuildTimeStruct =
|
let unique: BuildTimeStruct =
|
||||||
@ -68,7 +68,7 @@ async fn get_stats(db: &DatabaseConnection) -> anyhow::Result<ListStats> {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(anyhow::anyhow!("No Average build time"))?;
|
.ok_or(anyhow::anyhow!("No Average build time"))?;
|
||||||
|
|
||||||
let avg_build_time: u32 = unique.avg_build_time as u32;
|
let avg_build_time: u32 = unique.avg_build_time.unwrap_or(0.0) as u32;
|
||||||
|
|
||||||
// Count total packages
|
// Count total packages
|
||||||
let total_packages: u32 = Packages::find().count(db).await?.try_into()?;
|
let total_packages: u32 = Packages::find().count(db).await?.try_into()?;
|
||||||
|
@ -43,7 +43,7 @@ pub async fn get_info_by_name(pkg_name: &str) -> anyhow::Result<Package> {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_pkgbuild(url: &str, dest_dir: &str) -> anyhow::Result<String> {
|
pub async fn download_pkgbuild(url: &str, dest_dir: &str, clear_build_dir: bool) -> anyhow::Result<String> {
|
||||||
let (file_data, file_name) = match download_file(url).await {
|
let (file_data, file_name) = match download_file(url).await {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -51,6 +51,10 @@ pub async fn download_pkgbuild(url: &str, dest_dir: &str) -> anyhow::Result<Stri
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if clear_build_dir {
|
||||||
|
fs::remove_dir_all(dest_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the directory exists
|
// Check if the directory exists
|
||||||
if fs::metadata(dest_dir).is_err() {
|
if fs::metadata(dest_dir).is_err() {
|
||||||
// Create the directory if it does not exist
|
// Create the directory if it does not exist
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops::Add;
|
||||||
use crate::builder::types::Action;
|
use crate::builder::types::Action;
|
||||||
use crate::db::builds::ActiveModel;
|
use crate::db::builds::ActiveModel;
|
||||||
use crate::db::prelude::{Builds, Packages};
|
use crate::db::prelude::{Builds, Packages};
|
||||||
@ -5,15 +7,16 @@ use crate::db::{builds, packages, versions};
|
|||||||
use crate::repo::repo::add_pkg;
|
use crate::repo::repo::add_pkg;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set};
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set};
|
||||||
use std::ops::Add;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tokio::sync::broadcast::error::RecvError;
|
use tokio::sync::broadcast::error::RecvError;
|
||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
use tokio::sync::{broadcast, Semaphore};
|
use tokio::sync::{broadcast, Mutex, Semaphore};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
pub async fn init(db: DatabaseConnection, tx: Sender<Action>) {
|
pub async fn init(db: DatabaseConnection, tx: Sender<Action>) {
|
||||||
let semaphore = Arc::new(Semaphore::new(1));
|
let semaphore = Arc::new(Semaphore::new(1));
|
||||||
|
let job_handles: Arc<Mutex<HashMap<i32, JoinHandle<_>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(_result) = tx.subscribe().recv().await {
|
if let Ok(_result) = tx.subscribe().recv().await {
|
||||||
@ -28,12 +31,41 @@ pub async fn init(db: DatabaseConnection, tx: Sender<Action>) {
|
|||||||
build_model,
|
build_model,
|
||||||
db.clone(),
|
db.clone(),
|
||||||
semaphore.clone(),
|
semaphore.clone(),
|
||||||
|
job_handles.clone()
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
Action::Cancel(build_id) => {
|
||||||
|
let _ = cancel_build(build_id, job_handles.clone(), db.clone()).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_build(build_id: i32, job_handles: Arc<Mutex<HashMap<i32, JoinHandle<()>>>>, db: DatabaseConnection) -> anyhow::Result<()> {
|
||||||
|
let build = Builds::find_by_id(build_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(anyhow!("No build found"))?;
|
||||||
|
|
||||||
|
let mut build: builds::ActiveModel = build.into();
|
||||||
|
build.status = Set(Some(4));
|
||||||
|
build.end_time = Set(Some(
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs() as u32,
|
||||||
|
));
|
||||||
|
let _ = build.clone().update(&db).await;
|
||||||
|
|
||||||
|
job_handles
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.remove(&build.id.clone().unwrap())
|
||||||
|
.ok_or(anyhow!("No build found"))?
|
||||||
|
.abort();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn queue_package(
|
async fn queue_package(
|
||||||
@ -44,12 +76,14 @@ async fn queue_package(
|
|||||||
mut build_model: builds::ActiveModel,
|
mut build_model: builds::ActiveModel,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
semaphore: Arc<Semaphore>,
|
semaphore: Arc<Semaphore>,
|
||||||
|
job_handles: Arc<Mutex<HashMap<i32, JoinHandle<()>>>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let permits = Arc::clone(&semaphore);
|
let permits = Arc::clone(&semaphore);
|
||||||
|
let build_id = build_model.id.clone().unwrap();
|
||||||
|
|
||||||
// spawn new thread for each pkg build
|
// spawn new thread for each pkg build
|
||||||
// todo add queue and build two packages in parallel
|
// todo add queue and build two packages in parallel
|
||||||
tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let _permit = permits.acquire().await.unwrap();
|
let _permit = permits.acquire().await.unwrap();
|
||||||
|
|
||||||
// set build status to building
|
// set build status to building
|
||||||
@ -58,6 +92,7 @@ async fn queue_package(
|
|||||||
|
|
||||||
let _ = build_package(build_model, db, version_model, version, name, url).await;
|
let _ = build_package(build_model, db, version_model, version, name, url).await;
|
||||||
});
|
});
|
||||||
|
job_handles.lock().await.insert(build_id, handle);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +118,7 @@ async fn build_package(
|
|||||||
pkg.status = Set(0);
|
pkg.status = Set(0);
|
||||||
pkg = pkg.update(&db).await?.into();
|
pkg = pkg.update(&db).await?.into();
|
||||||
|
|
||||||
match add_pkg(url, version, name, tx).await {
|
match add_pkg(url, version, name, tx, false).await {
|
||||||
Ok(pkg_file_name) => {
|
Ok(pkg_file_name) => {
|
||||||
println!("successfully built package");
|
println!("successfully built package");
|
||||||
// update package success status
|
// update package success status
|
||||||
|
@ -9,4 +9,5 @@ pub enum Action {
|
|||||||
versions::ActiveModel,
|
versions::ActiveModel,
|
||||||
builds::ActiveModel,
|
builds::ActiveModel,
|
||||||
),
|
),
|
||||||
|
Cancel(i32),
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,9 @@ pub async fn add_pkg(
|
|||||||
version: String,
|
version: String,
|
||||||
name: String,
|
name: String,
|
||||||
tx: Sender<String>,
|
tx: Sender<String>,
|
||||||
|
clear_build_dir: bool,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let fname = download_pkgbuild(format!("{}{}", BASEURL, url).as_str(), "./builds").await?;
|
let fname = download_pkgbuild(format!("{}{}", BASEURL, url).as_str(), "./builds", clear_build_dir).await?;
|
||||||
let pkg_file_names = build_pkgbuild(
|
let pkg_file_names = build_pkgbuild(
|
||||||
format!("./builds/{fname}"),
|
format!("./builds/{fname}"),
|
||||||
version.as_str(),
|
version.as_str(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::env;
|
||||||
use crate::db::packages;
|
use crate::db::packages;
|
||||||
use crate::db::prelude::{Packages, Versions};
|
use crate::db::prelude::{Packages, Versions};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@ -5,11 +6,16 @@ use aur_rs::{Package, Request};
|
|||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::{sleep};
|
||||||
|
|
||||||
pub fn start_aur_version_checking(db: DatabaseConnection) {
|
pub fn start_aur_version_checking(db: DatabaseConnection) {
|
||||||
|
let default_version_check_interval = 10;
|
||||||
|
let check_interval = env::var("VERSION_CHECK_INTERVAL")
|
||||||
|
.map(|x| x.parse::<u64>().unwrap_or(default_version_check_interval))
|
||||||
|
.unwrap_or(default_version_check_interval);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
sleep(Duration::from_secs(10)).await;
|
sleep(Duration::from_secs(check_interval)).await;
|
||||||
loop {
|
loop {
|
||||||
println!("performing aur version checks");
|
println!("performing aur version checks");
|
||||||
match aur_check_versions(db.clone()).await {
|
match aur_check_versions(db.clone()).await {
|
||||||
|
@ -21,12 +21,17 @@ extension BuildsAPI on ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Build> getBuild(int id) async {
|
Future<Build> getBuild(int id) async {
|
||||||
final resp = await getRawClient().get("/build/${id}");
|
final resp = await getRawClient().get("/build/$id");
|
||||||
return Build.fromJson(resp.data);
|
return Build.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> deleteBuild(int id) async {
|
Future<bool> deleteBuild(int id) async {
|
||||||
final resp = await getRawClient().delete("/build/${id}");
|
final resp = await getRawClient().delete("/build/$id");
|
||||||
|
return resp.statusCode == 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> cancelBuild(int id) async {
|
||||||
|
final resp = await getRawClient().post("/build/$id/cancel");
|
||||||
return resp.statusCode == 400;
|
return resp.statusCode == 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import '../constants/color_constants.dart';
|
import '../constants/color_constants.dart';
|
||||||
import '../models/build.dart';
|
import '../models/build.dart';
|
||||||
import 'dashboard/your_packages.dart';
|
import '../utils/package_color.dart';
|
||||||
|
|
||||||
class BuildsTable extends StatelessWidget {
|
class BuildsTable extends StatelessWidget {
|
||||||
const BuildsTable({super.key, required this.data});
|
const BuildsTable({super.key, required this.data});
|
||||||
|
@ -3,15 +3,15 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class BuildsChart extends StatefulWidget {
|
class BuildsChart extends StatefulWidget {
|
||||||
const BuildsChart({
|
const BuildsChart({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.nrbuilds,
|
required this.nrbuilds,
|
||||||
required this.nrfailedbuilds,
|
required this.nrfailedbuilds,
|
||||||
required this.nrActiveBuilds,
|
required this.nrEnqueuedBuilds,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final int nrbuilds;
|
final int nrbuilds;
|
||||||
final int nrfailedbuilds;
|
final int nrfailedbuilds;
|
||||||
final int nrActiveBuilds;
|
final int nrEnqueuedBuilds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_BuildsChartState createState() => _BuildsChartState();
|
_BuildsChartState createState() => _BuildsChartState();
|
||||||
@ -88,10 +88,10 @@ class _BuildsChartState extends State<BuildsChart> {
|
|||||||
color: const Color(0xff0a7005),
|
color: const Color(0xff0a7005),
|
||||||
value: (widget.nrbuilds -
|
value: (widget.nrbuilds -
|
||||||
widget.nrfailedbuilds -
|
widget.nrfailedbuilds -
|
||||||
widget.nrActiveBuilds)
|
widget.nrEnqueuedBuilds)
|
||||||
.toDouble(),
|
.toDouble(),
|
||||||
title:
|
title:
|
||||||
"${((widget.nrbuilds - widget.nrfailedbuilds - widget.nrActiveBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
"${((widget.nrbuilds - widget.nrfailedbuilds - widget.nrEnqueuedBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
||||||
radius: radius,
|
radius: radius,
|
||||||
titleStyle: TextStyle(
|
titleStyle: TextStyle(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
@ -101,9 +101,9 @@ class _BuildsChartState extends State<BuildsChart> {
|
|||||||
case 2:
|
case 2:
|
||||||
return PieChartSectionData(
|
return PieChartSectionData(
|
||||||
color: const Color(0xFF0044AA),
|
color: const Color(0xFF0044AA),
|
||||||
value: (widget.nrActiveBuilds).toDouble(),
|
value: (widget.nrEnqueuedBuilds).toDouble(),
|
||||||
title:
|
title:
|
||||||
"${((widget.nrActiveBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
"${((widget.nrEnqueuedBuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
|
||||||
radius: radius,
|
radius: radius,
|
||||||
titleStyle: TextStyle(
|
titleStyle: TextStyle(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
|
@ -5,17 +5,11 @@ import 'package:aurcache/providers/api/builds_provider.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../constants/color_constants.dart';
|
import '../../constants/color_constants.dart';
|
||||||
|
import '../table_info.dart';
|
||||||
|
|
||||||
class RecentBuilds extends StatefulWidget {
|
class RecentBuilds extends StatelessWidget {
|
||||||
const RecentBuilds({
|
const RecentBuilds({super.key});
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<RecentBuilds> createState() => _RecentBuildsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecentBuildsState extends State<RecentBuilds> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -31,18 +25,20 @@ class _RecentBuildsState extends State<RecentBuilds> {
|
|||||||
"Recent Builds",
|
"Recent Builds",
|
||||||
style: Theme.of(context).textTheme.subtitle1,
|
style: Theme.of(context).textTheme.subtitle1,
|
||||||
),
|
),
|
||||||
SizedBox(
|
APIBuilder<BuildsProvider, List<Build>, BuildsDTO>(
|
||||||
width: double.infinity,
|
|
||||||
child: APIBuilder<BuildsProvider, List<Build>, BuildsDTO>(
|
|
||||||
key: const Key("Builds on dashboard"),
|
key: const Key("Builds on dashboard"),
|
||||||
dto: BuildsDTO(limit: 10),
|
dto: BuildsDTO(limit: 10),
|
||||||
interval: const Duration(seconds: 10),
|
interval: const Duration(seconds: 10),
|
||||||
onLoad: () => const Text("no data"),
|
onLoad: () => const Text("no data"),
|
||||||
onData: (t) {
|
onData: (t) {
|
||||||
return BuildsTable(data: t);
|
if (t.isEmpty) {
|
||||||
},
|
return const TableInfo(title: "You have no builds yet");
|
||||||
),
|
} else {
|
||||||
),
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity, child: BuildsTable(data: t)),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push("/builds");
|
context.push("/builds");
|
||||||
@ -51,7 +47,12 @@ class _RecentBuildsState extends State<RecentBuilds> {
|
|||||||
"List all Builds",
|
"List all Builds",
|
||||||
style: TextStyle(color: Colors.white.withOpacity(0.8)),
|
style: TextStyle(color: Colors.white.withOpacity(0.8)),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -35,10 +35,34 @@ class SidePanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: defaultPadding),
|
const SizedBox(height: defaultPadding),
|
||||||
BuildsChart(
|
nrbuilds > 0
|
||||||
|
? BuildsChart(
|
||||||
nrbuilds: nrbuilds,
|
nrbuilds: nrbuilds,
|
||||||
nrfailedbuilds: nrfailedbuilds,
|
nrfailedbuilds: nrfailedbuilds,
|
||||||
nrActiveBuilds: nrEnqueuedBuilds),
|
nrEnqueuedBuilds: nrEnqueuedBuilds)
|
||||||
|
: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
size: 42,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
Text("Add Packages to view Graph"),
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
SideCard(
|
SideCard(
|
||||||
color: const Color(0xff0a7005),
|
color: const Color(0xff0a7005),
|
||||||
title: "Successful Builds",
|
title: "Successful Builds",
|
||||||
|
@ -5,17 +5,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../constants/color_constants.dart';
|
import '../../constants/color_constants.dart';
|
||||||
import '../../models/package.dart';
|
import '../../models/package.dart';
|
||||||
|
import '../table_info.dart';
|
||||||
|
|
||||||
class YourPackages extends StatefulWidget {
|
class YourPackages extends StatelessWidget {
|
||||||
const YourPackages({
|
const YourPackages({super.key});
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<YourPackages> createState() => _YourPackagesState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _YourPackagesState extends State<YourPackages> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -31,19 +25,20 @@ class _YourPackagesState extends State<YourPackages> {
|
|||||||
"Your Packages",
|
"Your Packages",
|
||||||
style: Theme.of(context).textTheme.subtitle1,
|
style: Theme.of(context).textTheme.subtitle1,
|
||||||
),
|
),
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
APIBuilder<PackagesProvider, List<Package>, PackagesDTO>(
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: APIBuilder<PackagesProvider, List<Package>, PackagesDTO>(
|
|
||||||
key: const Key("Packages on dashboard"),
|
key: const Key("Packages on dashboard"),
|
||||||
interval: const Duration(seconds: 10),
|
interval: const Duration(seconds: 10),
|
||||||
dto: PackagesDTO(limit: 10),
|
dto: PackagesDTO(limit: 10),
|
||||||
onData: (data) {
|
onData: (data) {
|
||||||
return PackagesTable(data: data);
|
if (data.isEmpty) {
|
||||||
},
|
return const TableInfo(title: "You have no packages yet");
|
||||||
onLoad: () => const Text("No data"),
|
} else {
|
||||||
),
|
return Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: PackagesTable(data: data)),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push("/packages");
|
context.push("/packages");
|
||||||
@ -53,39 +48,14 @@ class _YourPackagesState extends State<YourPackages> {
|
|||||||
style: TextStyle(color: Colors.white.withOpacity(0.8)),
|
style: TextStyle(color: Colors.white.withOpacity(0.8)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
]),
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad: () => const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData switchSuccessIcon(int status) {
|
|
||||||
switch (status) {
|
|
||||||
case 0:
|
|
||||||
return Icons.watch_later_outlined;
|
|
||||||
case 1:
|
|
||||||
return Icons.check_circle_outline;
|
|
||||||
case 2:
|
|
||||||
return Icons.cancel_outlined;
|
|
||||||
case 3:
|
|
||||||
return Icons.pause_circle_outline;
|
|
||||||
default:
|
|
||||||
return Icons.question_mark_outlined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color switchSuccessColor(int status) {
|
|
||||||
switch (status) {
|
|
||||||
case 0:
|
|
||||||
return const Color(0xFF9D8D00);
|
|
||||||
case 1:
|
|
||||||
return const Color(0xFF0A6900);
|
|
||||||
case 2:
|
|
||||||
return const Color(0xff760707);
|
|
||||||
case 3:
|
|
||||||
return const Color(0xFF0044AA);
|
|
||||||
default:
|
|
||||||
return const Color(0xFF9D8D00);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,8 +9,8 @@ import '../models/package.dart';
|
|||||||
import '../providers/api/builds_provider.dart';
|
import '../providers/api/builds_provider.dart';
|
||||||
import '../providers/api/packages_provider.dart';
|
import '../providers/api/packages_provider.dart';
|
||||||
import '../providers/api/stats_provider.dart';
|
import '../providers/api/stats_provider.dart';
|
||||||
|
import '../utils/package_color.dart';
|
||||||
import 'confirm_popup.dart';
|
import 'confirm_popup.dart';
|
||||||
import 'dashboard/your_packages.dart';
|
|
||||||
|
|
||||||
class PackagesTable extends StatelessWidget {
|
class PackagesTable extends StatelessWidget {
|
||||||
const PackagesTable({super.key, required this.data});
|
const PackagesTable({super.key, required this.data});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../utils/responsive.dart';
|
import '../../utils/responsive.dart';
|
||||||
import '../../screens/dashboard_screen.dart';
|
|
||||||
import 'side_menu.dart';
|
import 'side_menu.dart';
|
||||||
|
|
||||||
class MenuShell extends StatelessWidget {
|
class MenuShell extends StatelessWidget {
|
||||||
|
@ -6,8 +6,8 @@ import '../../constants/color_constants.dart';
|
|||||||
|
|
||||||
class SideMenu extends StatelessWidget {
|
class SideMenu extends StatelessWidget {
|
||||||
const SideMenu({
|
const SideMenu({
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -20,13 +20,10 @@ class SideMenu extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// SizedBox(
|
SizedBox(
|
||||||
// height: defaultPadding * 3,
|
height: defaultPadding,
|
||||||
// ),
|
),
|
||||||
// Image.asset(
|
Icon(Icons.storage_sharp, size: 60, color: Colors.white54),
|
||||||
// "assets/logo/logo_icon.png",
|
|
||||||
// scale: 5,
|
|
||||||
// ),
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: defaultPadding,
|
height: defaultPadding,
|
||||||
),
|
),
|
||||||
@ -68,12 +65,11 @@ class SideMenu extends StatelessWidget {
|
|||||||
|
|
||||||
class DrawerListTile extends StatelessWidget {
|
class DrawerListTile extends StatelessWidget {
|
||||||
const DrawerListTile({
|
const DrawerListTile({
|
||||||
Key? key,
|
super.key,
|
||||||
// For selecting those three line once press "Command+D"
|
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.svgSrc,
|
required this.svgSrc,
|
||||||
required this.press,
|
required this.press,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final String title, svgSrc;
|
final String title, svgSrc;
|
||||||
final VoidCallback press;
|
final VoidCallback press;
|
||||||
|
30
frontend/lib/components/table_info.dart
Normal file
30
frontend/lib/components/table_info.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TableInfo extends StatelessWidget {
|
||||||
|
const TableInfo({super.key, required this.title});
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
size: 42,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
Text(title),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,9 @@ class _AurScreenState extends State<AurScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(),
|
appBar: AppBar(
|
||||||
|
title: const Text("AUR"),
|
||||||
|
),
|
||||||
body: MultiProvider(
|
body: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => PackagesProvider()),
|
ChangeNotifierProvider(create: (_) => PackagesProvider()),
|
||||||
|
@ -12,9 +12,9 @@ import 'package:provider/provider.dart';
|
|||||||
import '../api/API.dart';
|
import '../api/API.dart';
|
||||||
import '../components/confirm_popup.dart';
|
import '../components/confirm_popup.dart';
|
||||||
import '../components/dashboard/chart_card.dart';
|
import '../components/dashboard/chart_card.dart';
|
||||||
import '../components/dashboard/your_packages.dart';
|
|
||||||
import '../constants/color_constants.dart';
|
import '../constants/color_constants.dart';
|
||||||
import '../providers/api/build_provider.dart';
|
import '../providers/api/build_provider.dart';
|
||||||
|
import '../utils/package_color.dart';
|
||||||
|
|
||||||
class BuildScreen extends StatefulWidget {
|
class BuildScreen extends StatefulWidget {
|
||||||
const BuildScreen({super.key, required this.buildID});
|
const BuildScreen({super.key, required this.buildID});
|
||||||
@ -184,37 +184,7 @@ class _BuildScreenState extends State<BuildScreen> {
|
|||||||
height: 20,
|
height: 20,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: buildActions(buildData),
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final confirmResult = await showConfirmationDialog(
|
|
||||||
context,
|
|
||||||
"Delete Build",
|
|
||||||
"Are you sure to delete this Package?", () {
|
|
||||||
API.deleteBuild(widget.buildID);
|
|
||||||
context.pop();
|
|
||||||
}, null);
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"Delete",
|
|
||||||
style: TextStyle(color: Colors.redAccent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final buildid =
|
|
||||||
await API.updatePackage(id: buildData.pkg_id);
|
|
||||||
context.pushReplacement("/build/$buildid");
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"Retry",
|
|
||||||
style: TextStyle(color: Colors.orangeAccent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 15,
|
height: 15,
|
||||||
@ -233,7 +203,7 @@ class _BuildScreenState extends State<BuildScreen> {
|
|||||||
),
|
),
|
||||||
SideCard(
|
SideCard(
|
||||||
title: "Build Number",
|
title: "Build Number",
|
||||||
textRight: buildData.id.toString(),
|
textRight: "#${buildData.id}",
|
||||||
),
|
),
|
||||||
SideCard(
|
SideCard(
|
||||||
title: "Version",
|
title: "Version",
|
||||||
@ -257,6 +227,58 @@ class _BuildScreenState extends State<BuildScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> buildActions(Build build) {
|
||||||
|
if (build.status == 0) {
|
||||||
|
return [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showConfirmationDialog(
|
||||||
|
context, "Cancel Build", "Are you sure to cancel this Build?",
|
||||||
|
() {
|
||||||
|
API.cancelBuild(widget.buildID);
|
||||||
|
Provider.of<BuildProvider>(context, listen: false)
|
||||||
|
.refresh(context);
|
||||||
|
}, null);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Cancel",
|
||||||
|
style: TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showConfirmationDialog(
|
||||||
|
context, "Delete Build", "Are you sure to delete this Build?",
|
||||||
|
() {
|
||||||
|
API.deleteBuild(widget.buildID);
|
||||||
|
context.pop();
|
||||||
|
}, null);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Delete",
|
||||||
|
style: TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final buildid = await API.updatePackage(id: build.pkg_id);
|
||||||
|
context.pushReplacement("/build/$buildid");
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Retry",
|
||||||
|
style: TextStyle(color: Colors.orangeAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildPage(Build build) {
|
Widget _buildPage(Build build) {
|
||||||
switch (build.status) {
|
switch (build.status) {
|
||||||
case 3:
|
case 3:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:aurcache/components/builds_table.dart';
|
import 'package:aurcache/components/builds_table.dart';
|
||||||
import 'package:aurcache/components/api/APIBuilder.dart';
|
import 'package:aurcache/components/api/APIBuilder.dart';
|
||||||
|
import 'package:aurcache/components/table_info.dart';
|
||||||
import 'package:aurcache/providers/api/builds_provider.dart';
|
import 'package:aurcache/providers/api/builds_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -12,7 +13,9 @@ class BuildsScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(),
|
appBar: AppBar(
|
||||||
|
title: const Text("All Builds"),
|
||||||
|
),
|
||||||
body: MultiProvider(
|
body: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider<BuildsProvider>(
|
ChangeNotifierProvider<BuildsProvider>(
|
||||||
@ -41,7 +44,12 @@ class BuildsScreen extends StatelessWidget {
|
|||||||
interval: const Duration(seconds: 10),
|
interval: const Duration(seconds: 10),
|
||||||
onLoad: () => const Text("no data"),
|
onLoad: () => const Text("no data"),
|
||||||
onData: (data) {
|
onData: (data) {
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return const TableInfo(
|
||||||
|
title: "You have no builds yet");
|
||||||
|
} else {
|
||||||
return BuildsTable(data: data);
|
return BuildsTable(data: data);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -60,7 +60,6 @@ class _PackageScreenState extends State<PackageScreen> {
|
|||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final confirmResult =
|
|
||||||
await showConfirmationDialog(
|
await showConfirmationDialog(
|
||||||
context,
|
context,
|
||||||
"Force update Package",
|
"Force update Package",
|
||||||
@ -91,7 +90,6 @@ class _PackageScreenState extends State<PackageScreen> {
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final confirmResult =
|
|
||||||
await showConfirmationDialog(
|
await showConfirmationDialog(
|
||||||
context,
|
context,
|
||||||
"Delete Package",
|
"Delete Package",
|
||||||
@ -121,7 +119,7 @@ class _PackageScreenState extends State<PackageScreen> {
|
|||||||
style: TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: Colors.redAccent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 15,
|
width: 15,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
34
frontend/lib/utils/package_color.dart
Normal file
34
frontend/lib/utils/package_color.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
IconData switchSuccessIcon(int status) {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return Icons.watch_later_outlined;
|
||||||
|
case 1:
|
||||||
|
return Icons.check_circle_outline;
|
||||||
|
case 2:
|
||||||
|
return Icons.cancel_outlined;
|
||||||
|
case 3:
|
||||||
|
return Icons.pause_circle_outline;
|
||||||
|
case 4:
|
||||||
|
return Icons.remove_circle_outline;
|
||||||
|
default:
|
||||||
|
return Icons.question_mark_outlined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color switchSuccessColor(int status) {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return const Color(0xFF9D8D00);
|
||||||
|
case 1:
|
||||||
|
return const Color(0xFF0A6900);
|
||||||
|
case 4:
|
||||||
|
case 2:
|
||||||
|
return const Color(0xff760707);
|
||||||
|
case 3:
|
||||||
|
return const Color(0xFF0044AA);
|
||||||
|
default:
|
||||||
|
return const Color(0xFF9D8D00);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ extension TimeFormatter on DateTime {
|
|||||||
if (duration.inSeconds < 60) {
|
if (duration.inSeconds < 60) {
|
||||||
return '${duration.inSeconds} Second${_s(duration.inSeconds)} ago';
|
return '${duration.inSeconds} Second${_s(duration.inSeconds)} ago';
|
||||||
} else if (duration.inMinutes < 60) {
|
} else if (duration.inMinutes < 60) {
|
||||||
return '${duration.inMinutes} Minute${_s(duration.inMinutes)})} ago';
|
return '${duration.inMinutes} Minute${_s(duration.inMinutes)} ago';
|
||||||
} else if (duration.inHours < 24) {
|
} else if (duration.inHours < 24) {
|
||||||
return '${duration.inHours} Hour${_s(duration.inHours)} ago';
|
return '${duration.inHours} Hour${_s(duration.inHours)} ago';
|
||||||
} else if (duration.inDays < 30) {
|
} else if (duration.inDays < 30) {
|
||||||
|
BIN
res/imgs/screenshot1.png
Normal file
BIN
res/imgs/screenshot1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 311 KiB |
BIN
res/imgs/screenshot2.png
Normal file
BIN
res/imgs/screenshot2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 404 KiB |
BIN
res/imgs/screenshot3.png
Normal file
BIN
res/imgs/screenshot3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 KiB |
Loading…
Reference in New Issue
Block a user