diff --git a/backend/src/api/backend.rs b/backend/src/api/backend.rs index be997b6..c568d9f 100644 --- a/backend/src/api/backend.rs +++ b/backend/src/api/backend.rs @@ -1,14 +1,15 @@ use crate::api::add::okapi_add_operation_for_package_add_; use crate::api::add::package_add; -use crate::api::list::{get_build, okapi_add_operation_for_list_builds_}; +use crate::api::list::okapi_add_operation_for_get_build_; +use crate::api::list::okapi_add_operation_for_get_package_; use crate::api::list::okapi_add_operation_for_stats_; use crate::api::list::{build_output, okapi_add_operation_for_package_list_}; +use crate::api::list::{get_build, get_package, okapi_add_operation_for_list_builds_}; use crate::api::list::{list_builds, okapi_add_operation_for_search_}; use crate::api::list::{okapi_add_operation_for_build_output_, stats}; use crate::api::list::{package_list, search}; use crate::api::remove::okapi_add_operation_for_package_del_; use crate::api::remove::okapi_add_operation_for_version_del_; -use crate::api::list::okapi_add_operation_for_get_build_; use crate::api::remove::{package_del, version_del}; use rocket::Route; use rocket_okapi::openapi_get_routes; @@ -23,6 +24,7 @@ pub fn build_api() -> Vec { build_output, list_builds, stats, - get_build + get_build, + get_package ] } diff --git a/backend/src/api/list.rs b/backend/src/api/list.rs index 884fa7b..393dfd3 100644 --- a/backend/src/api/list.rs +++ b/backend/src/api/list.rs @@ -9,7 +9,7 @@ use rocket::serde::{Deserialize, Serialize}; use rocket::{get, State}; use rocket_okapi::okapi::schemars; use rocket_okapi::{openapi, JsonSchema}; -use sea_orm::{PaginatorTrait}; +use sea_orm::PaginatorTrait; use sea_orm::{ColumnTrait, QueryFilter}; use sea_orm::{DatabaseConnection, EntityTrait, FromQueryResult, QuerySelect, RelationTrait}; @@ -70,6 +70,32 @@ pub async fn package_list( Ok(Json(all)) } +#[openapi(tag = "test")] +#[get("/package/")] +pub async fn get_package( + db: &State, + id: u64, +) -> Result, NotFound> { + let db = db as &DatabaseConnection; + + let all: ListPackageModel = Packages::find() + .join_rev(JoinType::InnerJoin, versions::Relation::Packages.def()) + .filter(packages::Column::Id.eq(id)) + .select_only() + .column_as(versions::Column::Id.count(), "count") + .column(packages::Column::Name) + .column(packages::Column::Id) + .column(packages::Column::Status) + .group_by(packages::Column::Name) + .into_model::() + .one(db) + .await + .map_err(|e| NotFound(e.to_string()))? + .ok_or(NotFound("id not found".to_string()))?; + + Ok(Json(all)) +} + #[openapi(tag = "test")] #[get("/builds/output?&")] pub async fn build_output( @@ -172,7 +198,10 @@ pub async fn get_build( .column_as(packages::Column::Name, "pkg_name") .column(versions::Column::Version) .into_model::() - .one(db).await.map_err(|e| NotFound(e.to_string()))?.ok_or(NotFound("no item with id found".to_string()))?; + .one(db) + .await + .map_err(|e| NotFound(e.to_string()))? + .ok_or(NotFound("no item with id found".to_string()))?; Ok(Json(result)) } diff --git a/backend/src/api/remove.rs b/backend/src/api/remove.rs index 255f8f3..21c52a2 100644 --- a/backend/src/api/remove.rs +++ b/backend/src/api/remove.rs @@ -6,22 +6,12 @@ use rocket_okapi::okapi::schemars; use rocket_okapi::{openapi, JsonSchema}; use sea_orm::DatabaseConnection; -#[derive(Deserialize, JsonSchema)] -#[serde(crate = "rocket::serde")] -pub struct DelBody { - id: i32, -} - #[openapi(tag = "test")] -#[post("/packages/delete", data = "")] -pub async fn package_del( - db: &State, - input: Json, -) -> Result<(), String> { +#[post("/package/delete/")] +pub async fn package_del(db: &State, id: i32) -> Result<(), String> { let db = db as &DatabaseConnection; - let pkg_id = input.id.clone(); - remove_pkg(db, pkg_id).await.map_err(|e| e.to_string())?; + remove_pkg(db, id).await.map_err(|e| e.to_string())?; Ok(()) } diff --git a/frontend/lib/api/api_client.dart b/frontend/lib/api/api_client.dart index eb4d2fd..dd0e9d8 100644 --- a/frontend/lib/api/api_client.dart +++ b/frontend/lib/api/api_client.dart @@ -2,7 +2,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; class ApiClient { - static const String _apiBase = !kDebugMode ? "http://localhost:8081/api" : "api"; + static const String _apiBase = + kDebugMode ? "http://localhost:8081/api" : "api"; final Dio _dio = Dio(BaseOptions(baseUrl: _apiBase)); String? token; diff --git a/frontend/lib/api/builds.dart b/frontend/lib/api/builds.dart index 84ebeab..d90eb2a 100644 --- a/frontend/lib/api/builds.dart +++ b/frontend/lib/api/builds.dart @@ -2,8 +2,17 @@ import '../models/build.dart'; import 'api_client.dart'; extension BuildsAPI on ApiClient { - Future> listAllBuilds() async { - final resp = await getRawClient().get("/builds"); + Future> listAllBuilds({int? pkgID, int? limit}) async { + String uri = "/builds?"; + if (pkgID != null) { + uri += "pkgid=$pkgID"; + } + + if (limit != null) { + uri += "limit=$limit"; + } + + final resp = await getRawClient().get(uri); final responseObject = resp.data as List; final List packages = diff --git a/frontend/lib/api/packages.dart b/frontend/lib/api/packages.dart index 60ac6df..04c01ad 100644 --- a/frontend/lib/api/packages.dart +++ b/frontend/lib/api/packages.dart @@ -11,9 +11,21 @@ extension PackagesAPI on ApiClient { return packages; } + Future getPackage(int id) async { + final resp = await getRawClient().get("/package/$id"); + + final package = Package.fromJson(resp.data); + return package; + } + Future addPackage({bool force = false, required String name}) async { final resp = await getRawClient() .post("/packages/add", data: {'force_build': force, 'name': name}); print(resp.data); } + + Future deletePackage(int id) async { + final resp = await getRawClient().post("/package/delete/$id"); + return resp.statusCode == 200; + } } diff --git a/frontend/lib/components/build_output.dart b/frontend/lib/components/build_output.dart new file mode 100644 index 0000000..6263cff --- /dev/null +++ b/frontend/lib/components/build_output.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:aurcache/api/builds.dart'; +import 'package:flutter/material.dart'; + +import '../api/API.dart'; +import '../models/build.dart'; + +class BuildOutput extends StatefulWidget { + const BuildOutput({super.key, required this.build}); + + final Build build; + + @override + State createState() => _BuildOutputState(); +} + +class _BuildOutputState extends State { + late Future initialOutput; + + String output = ""; + Timer? outputTimer; + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 1, + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.vertical, //.horizontal + child: Padding( + padding: const EdgeInsets.only(left: 30, right: 15), + child: Text( + output, + style: const TextStyle( + fontSize: 16.0, + color: Colors.white, + ), + ), + ), + ), + ); + } + + @override + void initState() { + super.initState(); + initOutputLoader(); + } + + void initOutputLoader() { + initialOutput = API.getOutput(buildID: widget.build.id); + initialOutput.then((value) { + setState(() { + output = value; + }); + _scrollToBottom(); + }); + + // poll new output only if not finished + if (widget.build.status == 0) { + outputTimer = Timer.periodic(const Duration(seconds: 3), (Timer t) async { + print("refreshing output"); + final value = await API.getOutput( + buildID: widget.build.id, line: output.split("\n").length); + setState(() { + output += value; + }); + + _scrollToBottom(); + }); + } + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + // scroll to bottom + final scrollPosition = scrollController.position; + if (scrollPosition.viewportDimension < scrollPosition.maxScrollExtent) { + scrollController.animateTo( + scrollPosition.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + super.dispose(); + outputTimer?.cancel(); + } +} diff --git a/frontend/lib/components/builds_table.dart b/frontend/lib/components/builds_table.dart new file mode 100644 index 0000000..a3b383c --- /dev/null +++ b/frontend/lib/components/builds_table.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../constants/color_constants.dart'; +import '../models/build.dart'; +import 'dashboard/your_packages.dart'; + +class BuildsTable extends StatelessWidget { + const BuildsTable({super.key, required this.data}); + final List data; + + @override + Widget build(BuildContext context) { + return DataTable( + horizontalMargin: 0, + columnSpacing: defaultPadding, + columns: const [ + DataColumn( + label: Text("Build ID"), + ), + DataColumn( + label: Text("Package Name"), + ), + DataColumn( + label: Text("Version"), + ), + DataColumn( + label: Text("Status"), + ), + ], + rows: data.map((e) => buildDataRow(context, e)).toList(), + ); + } + + DataRow buildDataRow(BuildContext context, Build build) { + return DataRow( + cells: [ + DataCell(Text(build.id.toString())), + DataCell(Text(build.pkg_name)), + DataCell(Text(build.version)), + DataCell(IconButton( + icon: Icon( + switchSuccessIcon(build.status), + color: switchSuccessColor(build.status), + ), + onPressed: () { + context.push("/build/${build.id}"); + }, + )), + ], + ); + } +} diff --git a/frontend/lib/components/confirm_popup.dart b/frontend/lib/components/confirm_popup.dart new file mode 100644 index 0000000..5b2163e --- /dev/null +++ b/frontend/lib/components/confirm_popup.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +Future showDeleteConfirmationDialog(BuildContext context) async { + return (await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Stack( + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(false); // Dismiss dialog on outside tap + }, + child: Container( + color: Colors.black.withOpacity(0.5), // Adjust opacity for blur + ), + ), + // Delete confirmation dialog + AlertDialog( + title: Text('Confirm Delete'), + content: Text('Are you sure you want to delete this item?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text('Yes, Delete'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); // Dismiss dialog + }, + child: Text('Cancel'), + ), + ], + ), + ], + ); + }, + ))!; +} diff --git a/frontend/lib/components/dashboard/recent_builds.dart b/frontend/lib/components/dashboard/recent_builds.dart index 3bcf662..b1346f3 100644 --- a/frontend/lib/components/dashboard/recent_builds.dart +++ b/frontend/lib/components/dashboard/recent_builds.dart @@ -1,10 +1,14 @@ import 'dart:async'; import 'package:aurcache/api/builds.dart'; +import 'package:aurcache/components/builds_table.dart'; import 'package:aurcache/models/build.dart'; import 'package:aurcache/components/dashboard/your_packages.dart'; +import 'package:aurcache/providers/APIBuilder.dart'; +import 'package:aurcache/providers/builds_provider.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import '../../api/API.dart'; import '../../constants/color_constants.dart'; @@ -19,27 +23,6 @@ class RecentBuilds extends StatefulWidget { } class _RecentBuildsState extends State { - late Future> dataFuture; - Timer? timer; - - @override - void initState() { - super.initState(); - dataFuture = API.listAllBuilds(); - - timer = Timer.periodic( - const Duration(seconds: 10), - (Timer t) => setState(() { - dataFuture = API.listAllBuilds(); - })); - } - - @override - void dispose() { - super.dispose(); - timer?.cancel(); - } - @override Widget build(BuildContext context) { return Container( @@ -57,57 +40,26 @@ class _RecentBuildsState extends State { ), SizedBox( width: double.infinity, - child: FutureBuilder( - future: dataFuture, - builder: (context, snapshot) { - if (snapshot.hasData) { - return DataTable( - horizontalMargin: 0, - columnSpacing: defaultPadding, - columns: const [ - DataColumn( - label: Text("Build ID"), - ), - DataColumn( - label: Text("Package Name"), - ), - DataColumn( - label: Text("Version"), - ), - DataColumn( - label: Text("Status"), - ), - ], - rows: snapshot.data! - .map((e) => recentUserDataRow(e)) - .toList(), - ); - } else { - return const Text("no data"); - } - }), + child: APIBuilder, BuildsDTO>( + dto: BuildsDTO(limit: 10), + interval: const Duration(seconds: 10), + onLoad: () => const Text("no data"), + onData: (t) { + return BuildsTable(data: t); + }, + ), ), + ElevatedButton( + onPressed: () { + context.push("/builds"); + }, + child: Text( + "List all Builds", + style: TextStyle(color: Colors.white.withOpacity(0.8)), + ), + ) ], ), ); } - - DataRow recentUserDataRow(Build build) { - return DataRow( - cells: [ - DataCell(Text(build.id.toString())), - DataCell(Text(build.pkg_name)), - DataCell(Text(build.version)), - DataCell(IconButton( - icon: Icon( - switchSuccessIcon(build.status), - color: switchSuccessColor(build.status), - ), - onPressed: () { - context.push("/build/${build.id}"); - }, - )), - ], - ); - } } diff --git a/frontend/lib/components/dashboard/search_field.dart b/frontend/lib/components/dashboard/search_field.dart index 8175f30..dbb7976 100644 --- a/frontend/lib/components/dashboard/search_field.dart +++ b/frontend/lib/components/dashboard/search_field.dart @@ -1,9 +1,13 @@ import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/providers/builds_provider.dart'; +import 'package:aurcache/providers/stats_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; import '../../api/API.dart'; import '../../constants/color_constants.dart'; +import '../../providers/packages_provider.dart'; class SearchField extends StatelessWidget { SearchField({ @@ -25,9 +29,14 @@ class SearchField extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(10)), ), suffixIcon: InkWell( - onTap: () { + onTap: () async { // todo this is only temporary -> add this to a proper page - API.addPackage(name: controller.text); + await API.addPackage(name: controller.text, force: true); + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false).refresh(context); }, child: Container( padding: EdgeInsets.all(defaultPadding * 0.75), diff --git a/frontend/lib/components/dashboard/your_packages.dart b/frontend/lib/components/dashboard/your_packages.dart index c2c4357..b3d8167 100644 --- a/frontend/lib/components/dashboard/your_packages.dart +++ b/frontend/lib/components/dashboard/your_packages.dart @@ -1,11 +1,18 @@ import 'dart:async'; import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/providers/APIBuilder.dart'; +import 'package:aurcache/providers/packages_provider.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import '../../api/API.dart'; import '../../constants/color_constants.dart'; import '../../models/package.dart'; +import '../../providers/builds_provider.dart'; +import '../../providers/stats_provider.dart'; +import '../confirm_popup.dart'; class YourPackages extends StatefulWidget { const YourPackages({ @@ -17,27 +24,6 @@ class YourPackages extends StatefulWidget { } class _YourPackagesState extends State { - late Future> dataFuture; - Timer? timer; - - @override - void initState() { - super.initState(); - dataFuture = API.listPackages(); - - timer = Timer.periodic( - const Duration(seconds: 10), - (Timer t) => setState(() { - dataFuture = API.listPackages(); - })); - } - - @override - void dispose() { - super.dispose(); - timer?.cancel(); - } - @override Widget build(BuildContext context) { return Container( @@ -57,10 +43,11 @@ class _YourPackagesState extends State { //scrollDirection: Axis.horizontal, child: SizedBox( width: double.infinity, - child: FutureBuilder( - builder: (context, snapshot) { - if (snapshot.hasData) { - return DataTable( + child: APIBuilder, Object>( + key: GlobalKey(), + interval: const Duration(seconds: 10), + onData: (data) { + return DataTable( horizontalMargin: 0, columnSpacing: defaultPadding, columns: const [ @@ -80,15 +67,11 @@ class _YourPackagesState extends State { label: Text("Action"), ), ], - rows: snapshot.data! + rows: data .map((e) => buildDataRow(e)) - .toList(growable: false), - ); - } else { - return const Text("No data"); - } + .toList(growable: false)); }, - future: dataFuture, + onLoad: () => const Text("No data"), ), ), ), @@ -117,7 +100,9 @@ class _YourPackagesState extends State { children: [ TextButton( child: const Text('View', style: TextStyle(color: greenColor)), - onPressed: () {}, + onPressed: () { + context.push("/package/${package.id}"); + }, ), const SizedBox( width: 6, @@ -125,7 +110,21 @@ class _YourPackagesState extends State { TextButton( child: const Text("Delete", style: TextStyle(color: Colors.redAccent)), - onPressed: () {}, + onPressed: () async { + final confirmResult = + await showDeleteConfirmationDialog(context); + if (!confirmResult) return; + + final succ = await API.deletePackage(package.id); + if (succ) { + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + } + }, ), ], ), diff --git a/frontend/lib/components/router.dart b/frontend/lib/components/router.dart index 6a980a2..6789c66 100644 --- a/frontend/lib/components/router.dart +++ b/frontend/lib/components/router.dart @@ -1,6 +1,8 @@ import 'package:aurcache/screens/build_screen.dart'; +import 'package:aurcache/screens/builds_screen.dart'; import 'package:aurcache/screens/dashboard_screen.dart'; import 'package:aurcache/components/menu_shell.dart'; +import 'package:aurcache/screens/package_screen.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -21,15 +23,24 @@ final appRouter = GoRouter( GoRoute( path: '/', builder: (context, state) => DashboardScreen(), - routes: [ - GoRoute( - path: 'build/:id', - builder: (context, state) { - final id = int.parse(state.pathParameters['id']!); - return BuildScreen(buildID: id); - }, - ), - ] + ), + GoRoute( + path: '/build/:id', + builder: (context, state) { + final id = int.parse(state.pathParameters['id']!); + return BuildScreen(buildID: id); + }, + ), + GoRoute( + path: '/builds', + builder: (context, state) => const BuildsScreen(), + ), + GoRoute( + path: '/package/:id', + builder: (context, state) { + final id = int.parse(state.pathParameters['id']!); + return PackageScreen(pkgID: id); + }, ), ], ), diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index cf7e64b..30e2e80 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,7 +1,13 @@ import 'package:aurcache/components/router.dart'; +import 'package:aurcache/providers/build_provider.dart'; +import 'package:aurcache/providers/builds_provider.dart'; +import 'package:aurcache/providers/package_provider.dart'; +import 'package:aurcache/providers/packages_provider.dart'; +import 'package:aurcache/providers/stats_provider.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; import 'constants/color_constants.dart'; void main() { @@ -14,22 +20,31 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp.router( - routerConfig: appRouter, - debugShowCheckedModeBanner: false, - title: 'Smart Dashboard - Admin Panel v0.1 ', - theme: ThemeData.dark().copyWith( - appBarTheme: const AppBarTheme(backgroundColor: bgColor, elevation: 0), - scaffoldBackgroundColor: bgColor, - primaryColor: greenColor, - dialogBackgroundColor: secondaryColor, - textTheme: GoogleFonts.openSansTextTheme(Theme.of(context).textTheme) - .apply(bodyColor: Colors.white), - canvasColor: secondaryColor, + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => StatsProvider()), + ChangeNotifierProvider( + create: (_) => PackagesProvider()), + ChangeNotifierProvider(create: (_) => BuildsProvider()), + ChangeNotifierProvider( + create: (_) => PackageProvider()), + ChangeNotifierProvider(create: (_) => BuildProvider()), + ], + child: MaterialApp.router( + routerConfig: appRouter, + debugShowCheckedModeBanner: false, + title: 'AURCache', + theme: ThemeData.dark().copyWith( + appBarTheme: + const AppBarTheme(backgroundColor: bgColor, elevation: 0), + scaffoldBackgroundColor: bgColor, + primaryColor: greenColor, + dialogBackgroundColor: secondaryColor, + textTheme: GoogleFonts.openSansTextTheme(Theme.of(context).textTheme) + .apply(bodyColor: Colors.white), + canvasColor: secondaryColor, + ), ), - routeInformationParser: appRouter.routeInformationParser, - routeInformationProvider: appRouter.routeInformationProvider, - routerDelegate: appRouter.routerDelegate, ); } } diff --git a/frontend/lib/providers/APIBuilder.dart b/frontend/lib/providers/APIBuilder.dart new file mode 100644 index 0000000..12ca755 --- /dev/null +++ b/frontend/lib/providers/APIBuilder.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'BaseProvider.dart'; + +class APIBuilder extends StatefulWidget { + const APIBuilder( + {super.key, + required this.onLoad, + required this.onData, + this.interval, + this.dto}); + + final DTO? dto; + final Duration? interval; + final Widget Function() onLoad; + final Widget Function(K t) onData; + + @override + State> createState() => _APIBuilderState(); +} + +class _APIBuilderState + extends State> { + Timer? timer; + + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).loadFuture(context, dto: widget.dto); + + if (widget.interval != null) { + timer = Timer.periodic(widget.interval!, (Timer t) { + final RenderObject? box = context.findRenderObject(); + print(box); + print(context.mounted); + + Provider.of(context, listen: false) + .refresh(context, dto: widget.dto); + }); + } + } + + @override + void dispose() { + super.dispose(); + timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + final Future fut = Provider.of(context).data as Future; + + return FutureBuilder( + future: fut, + builder: (context, snapshot) { + if (snapshot.hasData) { + return widget.onData(snapshot.data!); + } else { + return widget.onLoad(); + } + }, + ); + } +} diff --git a/frontend/lib/providers/BaseProvider.dart b/frontend/lib/providers/BaseProvider.dart new file mode 100644 index 0000000..c2ca658 --- /dev/null +++ b/frontend/lib/providers/BaseProvider.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +abstract class BaseProvider with ChangeNotifier { + late Future data; + + loadFuture(context, {DTO? dto}); + + refresh(context, {DTO? dto}) { + loadFuture(context, dto: dto); + notifyListeners(); + } +} diff --git a/frontend/lib/providers/build_provider.dart b/frontend/lib/providers/build_provider.dart new file mode 100644 index 0000000..6acfc72 --- /dev/null +++ b/frontend/lib/providers/build_provider.dart @@ -0,0 +1,19 @@ +import 'package:aurcache/api/builds.dart'; + +import '../api/API.dart'; +import '../models/build.dart'; +import 'BaseProvider.dart'; + +class BuildDTO { + final int buildID; + + BuildDTO({required this.buildID}); +} + +class BuildProvider extends BaseProvider { + @override + loadFuture(context, {dto}) { + // todo search solution to force an exising dto + data = API.getBuild(dto!.buildID); + } +} diff --git a/frontend/lib/providers/builds_provider.dart b/frontend/lib/providers/builds_provider.dart new file mode 100644 index 0000000..deeea50 --- /dev/null +++ b/frontend/lib/providers/builds_provider.dart @@ -0,0 +1,23 @@ +import 'package:aurcache/api/builds.dart'; + +import '../api/API.dart'; +import '../models/build.dart'; +import 'BaseProvider.dart'; + +class BuildsDTO { + final int? pkgID; + final int? limit; + + BuildsDTO({this.pkgID, this.limit}); +} + +class BuildsProvider extends BaseProvider, BuildsDTO> { + @override + loadFuture(context, {dto}) { + if (dto != null) { + data = API.listAllBuilds(pkgID: dto.pkgID, limit: dto.limit); + } else { + data = API.listAllBuilds(); + } + } +} diff --git a/frontend/lib/providers/menu_provider.dart b/frontend/lib/providers/menu_provider.dart deleted file mode 100644 index 8b13789..0000000 --- a/frontend/lib/providers/menu_provider.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/lib/providers/package_provider.dart b/frontend/lib/providers/package_provider.dart new file mode 100644 index 0000000..74e485c --- /dev/null +++ b/frontend/lib/providers/package_provider.dart @@ -0,0 +1,19 @@ +import 'package:aurcache/api/packages.dart'; + +import '../api/API.dart'; +import '../models/package.dart'; +import 'BaseProvider.dart'; + +class PackageDTO { + final int pkgID; + + PackageDTO({required this.pkgID}); +} + +class PackageProvider extends BaseProvider { + @override + loadFuture(context, {dto}) { + // todo search solution to force an exising dto + data = API.getPackage(dto!.pkgID); + } +} diff --git a/frontend/lib/providers/packages_provider.dart b/frontend/lib/providers/packages_provider.dart new file mode 100644 index 0000000..a7b3cb4 --- /dev/null +++ b/frontend/lib/providers/packages_provider.dart @@ -0,0 +1,11 @@ +import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/providers/BaseProvider.dart'; + +import '../api/API.dart'; + +class PackagesProvider extends BaseProvider { + @override + loadFuture(context, {dto}) { + data = API.listPackages(); + } +} diff --git a/frontend/lib/providers/stats_provider.dart b/frontend/lib/providers/stats_provider.dart new file mode 100644 index 0000000..6565aa5 --- /dev/null +++ b/frontend/lib/providers/stats_provider.dart @@ -0,0 +1,11 @@ +import 'package:aurcache/api/statistics.dart'; + +import '../api/API.dart'; +import 'BaseProvider.dart'; + +class StatsProvider extends BaseProvider { + @override + loadFuture(context, {dto}) { + data = API.listStats(); + } +} diff --git a/frontend/lib/screens/build_screen.dart b/frontend/lib/screens/build_screen.dart index b802a66..9d022e6 100644 --- a/frontend/lib/screens/build_screen.dart +++ b/frontend/lib/screens/build_screen.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'package:aurcache/api/builds.dart'; +import 'package:aurcache/components/build_output.dart'; import 'package:aurcache/models/build.dart'; +import 'package:aurcache/providers/APIBuilder.dart'; +import 'package:aurcache/providers/build_provider.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -18,146 +21,54 @@ class BuildScreen extends StatefulWidget { } class _BuildScreenState extends State { - late Future buildData; - late Future initialOutput; - - String output = ""; - Timer? outputTimer, buildDataTimer; - final scrollController = ScrollController(); - @override Widget build(BuildContext context) { return Scaffold( - body: FutureBuilder( - future: buildData, - builder: (context, snapshot) { - if (snapshot.hasData) { - final buildData = snapshot.data!; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox( - width: 10, - ), - IconButton( - icon: Icon( - switchSuccessIcon(buildData.status), - color: switchSuccessColor(buildData.status), - ), - onPressed: () { - context.replace("/build/${buildData.id}"); - }, - ), - const SizedBox( - width: 10, - ), - Text( - buildData.pkg_name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox( - width: 10, - ), - const Text("triggered 2 months ago") - ], - ), - const SizedBox( - height: 15, - ), - Expanded( - flex: 1, - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.vertical, //.horizontal - child: Padding( - padding: const EdgeInsets.only(left: 30, right: 15), - child: Text( - output, - style: const TextStyle( - fontSize: 16.0, - color: Colors.white, - ), - ), - ), + body: APIBuilder( + dto: BuildDTO(buildID: widget.buildID), + interval: const Duration(seconds: 10), + onLoad: () => const Text("no data"), + onData: (buildData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + width: 10, ), - ), - ], - ); - } else { - return const Text("loading build"); - } + IconButton( + icon: Icon( + switchSuccessIcon(buildData.status), + color: switchSuccessColor(buildData.status), + ), + onPressed: () { + context.replace("/build/${buildData.id}"); + }, + ), + const SizedBox( + width: 10, + ), + Text( + buildData.pkg_name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox( + width: 10, + ), + const Text("triggered 2 months ago") + ], + ), + const SizedBox( + height: 15, + ), + BuildOutput(build: buildData) + ], + ); }), appBar: AppBar(), ); } - - @override - void initState() { - super.initState(); - - initBuildDataLoader(); - initOutputLoader(); - } - - void initBuildDataLoader() { - buildData = API.getBuild(widget.buildID); - buildDataTimer = Timer.periodic(const Duration(seconds: 10), (t) { - setState(() { - buildData = API.getBuild(widget.buildID); - }); - }); - } - - void initOutputLoader() { - initialOutput = API.getOutput(buildID: widget.buildID); - initialOutput.then((value) { - setState(() { - output = value; - }); - _scrollToBottom(); - }); - - buildData.then((value) { - // poll new output only if not finished - if (value.status == 0) { - outputTimer = - Timer.periodic(const Duration(seconds: 3), (Timer t) async { - print("refreshing output"); - final value = await API.getOutput( - buildID: widget.buildID, line: output.split("\n").length); - setState(() { - output += value; - }); - - _scrollToBottom(); - }); - } - }); - } - - void _scrollToBottom() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // scroll to bottom - final scrollPosition = scrollController.position; - if (scrollPosition.viewportDimension < scrollPosition.maxScrollExtent) { - scrollController.animateTo( - scrollPosition.maxScrollExtent, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - } - }); - } - - @override - void dispose() { - super.dispose(); - outputTimer?.cancel(); - buildDataTimer?.cancel(); - } } diff --git a/frontend/lib/screens/builds_screen.dart b/frontend/lib/screens/builds_screen.dart new file mode 100644 index 0000000..3315556 --- /dev/null +++ b/frontend/lib/screens/builds_screen.dart @@ -0,0 +1,47 @@ +import 'package:aurcache/components/builds_table.dart'; +import 'package:aurcache/providers/APIBuilder.dart'; +import 'package:aurcache/providers/builds_provider.dart'; +import 'package:flutter/material.dart'; +import '../constants/color_constants.dart'; +import '../models/build.dart'; + +class BuildsScreen extends StatelessWidget { + const BuildsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Padding( + padding: const EdgeInsets.all(defaultPadding), + child: Container( + padding: const EdgeInsets.all(defaultPadding), + decoration: const BoxDecoration( + color: secondaryColor, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "All Builds", + style: Theme.of(context).textTheme.subtitle1, + ), + SizedBox( + width: double.infinity, + child: APIBuilder, Object>( + interval: const Duration(seconds: 10), + onLoad: () => const Text("no data"), + onData: (data) { + return BuildsTable(data: data); + }), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/screens/dashboard_screen.dart b/frontend/lib/screens/dashboard_screen.dart index b7e8065..a727f84 100644 --- a/frontend/lib/screens/dashboard_screen.dart +++ b/frontend/lib/screens/dashboard_screen.dart @@ -1,5 +1,8 @@ import 'package:aurcache/api/statistics.dart'; +import 'package:aurcache/providers/APIBuilder.dart'; +import 'package:aurcache/providers/stats_provider.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../api/API.dart'; import '../components/dashboard/header.dart'; @@ -11,70 +14,70 @@ import '../components/dashboard/recent_builds.dart'; import '../components/dashboard/your_packages.dart'; import '../components/dashboard/side_panel.dart'; -class DashboardScreen extends StatelessWidget { - final stats = API.listStats(); +class DashboardScreen extends StatefulWidget { + @override + State createState() => _DashboardScreenState(); +} +class _DashboardScreenState extends State { @override Widget build(BuildContext context) { - return FutureBuilder( - future: stats, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - final Stats stats = snapshot.data!; - - return SafeArea( - child: SingleChildScrollView( - child: Container( - padding: const EdgeInsets.all(defaultPadding), - child: Column( + return APIBuilder( + onData: (stats) { + return SafeArea( + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(defaultPadding), + child: Column( + children: [ + const Header(), + const SizedBox(height: defaultPadding), + QuickInfoBanner( + stats: stats, + ), + const SizedBox(height: defaultPadding), + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Header(), - const SizedBox(height: defaultPadding), - QuickInfoBanner( - stats: stats, - ), - const SizedBox(height: defaultPadding), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 5, - child: Column( - children: [ - const YourPackages(), - const SizedBox(height: defaultPadding), - const RecentBuilds(), - if (Responsive.isMobile(context)) - const SizedBox(height: defaultPadding), - if (Responsive.isMobile(context)) - SidePanel( - nrbuilds: stats.total_builds, - nrfailedbuilds: stats.failed_builds, - nrActiveBuilds: stats.active_builds), - ], - ), - ), - if (!Responsive.isMobile(context)) - const SizedBox(width: defaultPadding), - // On Mobile means if the screen is less than 850 we dont want to show it - if (!Responsive.isMobile(context)) - Expanded( - flex: 2, - child: SidePanel( + Expanded( + flex: 5, + child: Column( + children: [ + const YourPackages(), + const SizedBox(height: defaultPadding), + const RecentBuilds(), + if (Responsive.isMobile(context)) + const SizedBox(height: defaultPadding), + if (Responsive.isMobile(context)) + SidePanel( nrbuilds: stats.total_builds, nrfailedbuilds: stats.failed_builds, nrActiveBuilds: stats.active_builds), - ), - ], - ) + ], + ), + ), + if (!Responsive.isMobile(context)) + const SizedBox(width: defaultPadding), + // On Mobile means if the screen is less than 850 we dont want to show it + if (!Responsive.isMobile(context)) + Expanded( + flex: 2, + child: SidePanel( + nrbuilds: stats.total_builds, + nrfailedbuilds: stats.failed_builds, + nrActiveBuilds: stats.active_builds), + ), ], - ), - ), + ) + ], ), - ); - } else { - return const Text("loading"); - } - }); + ), + ), + ); + }, + onLoad: () { + return Text("loading"); + }, + ); } } diff --git a/frontend/lib/screens/package_screen.dart b/frontend/lib/screens/package_screen.dart new file mode 100644 index 0000000..3cd8468 --- /dev/null +++ b/frontend/lib/screens/package_screen.dart @@ -0,0 +1,114 @@ +import 'dart:async'; + +import 'package:aurcache/api/builds.dart'; +import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/providers/APIBuilder.dart'; +import 'package:aurcache/providers/builds_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../api/API.dart'; +import '../components/builds_table.dart'; +import '../components/confirm_popup.dart'; +import '../constants/color_constants.dart'; +import '../models/build.dart'; +import '../models/package.dart'; +import '../providers/package_provider.dart'; +import '../providers/packages_provider.dart'; + +class PackageScreen extends StatefulWidget { + const PackageScreen({super.key, required this.pkgID}); + + final int pkgID; + + @override + State createState() => _PackageScreenState(); +} + +class _PackageScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: APIBuilder( + dto: PackageDTO(pkgID: widget.pkgID), + onLoad: () => const Text("loading"), + onData: (pkg) { + return Padding( + padding: const EdgeInsets.all(defaultPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.only(left: 15), + child: Text( + pkg.name, + style: const TextStyle(fontSize: 32), + ), + ), + Container( + margin: const EdgeInsets.only(right: 15), + child: ElevatedButton( + onPressed: () async { + final confirmResult = + await showDeleteConfirmationDialog(context); + if (!confirmResult) return; + + final succ = await API.deletePackage(pkg.id); + if (succ) { + context.pop(); + } + }, + child: const Text( + "Delete", + style: TextStyle(color: Colors.redAccent), + ), + ), + ) + ], + ), + const SizedBox( + height: 25, + ), + Container( + padding: const EdgeInsets.all(defaultPadding), + decoration: const BoxDecoration( + color: secondaryColor, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Builds of ${pkg.name}", + style: Theme.of(context).textTheme.subtitle1, + ), + SizedBox( + width: double.infinity, + child: APIBuilder, + BuildsDTO>( + key: GlobalKey(), + dto: BuildsDTO(pkgID: pkg.id), + interval: const Duration(seconds: 5), + onData: (data) { + return BuildsTable(data: data); + }, + onLoad: () => const Text("no data"), + ), + ), + ], + ), + ), + ) + ], + ), + ); + }), + ); + } +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index a1b8bdb..09b5909 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -208,6 +208,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -296,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.7" + provider: + dependency: "direct main" + description: + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + url: "https://pub.dev" + source: hosted + version: "6.1.1" sky_engine: dependency: transitive description: flutter diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 3783e59..73ae70b 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: google_fonts: ^6.1.0 dio: ^5.3.3 go_router: ^13.0.0 + provider: ^6.1.1 dev_dependencies: flutter_test: