add endpoint for general stats

load build data to graph
redesign top info tiles
place add button on header
folder restructure
This commit is contained in:
lukas-heiligenbrunner 2023-12-30 00:45:33 +01:00
parent ce7a260760
commit 600c2057fe
38 changed files with 563 additions and 886 deletions

View File

@ -1,9 +1,10 @@
use crate::api::add::okapi_add_operation_for_package_add_; use crate::api::add::okapi_add_operation_for_package_add_;
use crate::api::add::package_add; use crate::api::add::package_add;
use crate::api::list::okapi_add_operation_for_build_output_;
use crate::api::list::okapi_add_operation_for_list_builds_; use crate::api::list::okapi_add_operation_for_list_builds_;
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::{build_output, okapi_add_operation_for_package_list_};
use crate::api::list::{list_builds, okapi_add_operation_for_search_}; 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::list::{package_list, search};
use crate::api::remove::okapi_add_operation_for_package_del_; use crate::api::remove::okapi_add_operation_for_package_del_;
use crate::api::remove::okapi_add_operation_for_version_del_; use crate::api::remove::okapi_add_operation_for_version_del_;
@ -20,5 +21,6 @@ pub fn build_api() -> Vec<Route> {
version_del, version_del,
build_output, build_output,
list_builds, list_builds,
stats
] ]
} }

View File

@ -2,14 +2,18 @@ use crate::aur::aur::query_aur;
use crate::db::migration::JoinType; use crate::db::migration::JoinType;
use crate::db::prelude::{Builds, Packages}; use crate::db::prelude::{Builds, Packages};
use crate::db::{builds, packages, versions}; use crate::db::{builds, packages, versions};
use crate::utils::dir_size::dir_size;
use rocket::response::status::NotFound; use rocket::response::status::NotFound;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::{get, State}; use rocket::{get, State};
use rocket_okapi::okapi::schemars; use rocket_okapi::okapi::schemars;
use rocket_okapi::{openapi, JsonSchema}; use rocket_okapi::{openapi, JsonSchema};
use sea_orm::PaginatorTrait;
use sea_orm::{ColumnTrait, QueryFilter}; use sea_orm::{ColumnTrait, QueryFilter};
use sea_orm::{DatabaseConnection, EntityTrait, FromQueryResult, QuerySelect, RelationTrait}; use sea_orm::{DatabaseConnection, EntityTrait, FromQueryResult, QuerySelect, RelationTrait};
use std::path::PathBuf;
use std::{fs, io};
#[derive(Serialize, JsonSchema)] #[derive(Serialize, JsonSchema)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
@ -149,3 +153,65 @@ pub async fn list_builds(
Ok(Json(build)) Ok(Json(build))
} }
#[derive(FromQueryResult, Deserialize, JsonSchema, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct ListStats {
total_builds: u32,
failed_builds: u32,
avg_queue_wait_time: u32,
avg_build_time: u32,
repo_storage_size: u32,
active_builds: u32,
total_packages: u32,
}
#[openapi(tag = "test")]
#[get("/stats")]
pub async fn stats(db: &State<DatabaseConnection>) -> Result<Json<ListStats>, NotFound<String>> {
let db = db as &DatabaseConnection;
return match get_stats(db).await {
Ok(v) => Ok(Json(v)),
Err(e) => Err(NotFound(e.to_string())),
};
}
async fn get_stats(db: &DatabaseConnection) -> anyhow::Result<ListStats> {
// Count total builds
let total_builds: u32 = Builds::find().count(db).await?.try_into()?;
// Count failed builds
let failed_builds: u32 = Builds::find()
.filter(builds::Column::Status.eq(2))
.count(db)
.await?
.try_into()?;
// todo implement this values somehow
let avg_queue_wait_time: u32 = 42;
let avg_build_time: u32 = 42;
// Calculate repo storage size
let repo_storage_size: u32 = dir_size("repo/").unwrap_or(0).try_into()?;
// Count active builds
let active_builds: u32 = Builds::find()
.filter(builds::Column::Status.eq(0))
.count(db)
.await?
.try_into()?;
// Count total packages
let total_packages: u32 = Packages::find().count(db).await?.try_into()?;
Ok(ListStats {
total_builds,
failed_builds,
avg_queue_wait_time,
avg_build_time,
repo_storage_size,
active_builds,
total_packages,
})
}

View File

@ -4,6 +4,7 @@ mod builder;
mod db; mod db;
mod pkgbuild; mod pkgbuild;
mod repo; mod repo;
mod utils;
use crate::api::backend; use crate::api::backend;
#[cfg(feature = "static")] #[cfg(feature = "static")]

View File

@ -0,0 +1,17 @@
use std::path::PathBuf;
use std::{fs, io};
pub fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
dir.try_fold(0, |acc, file| {
let file = file?;
let size = match file.metadata()? {
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
data => data.len(),
};
Ok(acc + size)
})
}
dir_size(fs::read_dir(path.into())?)
}

1
backend/src/utils/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod dir_size;

View File

@ -1,4 +1,4 @@
import '../core/models/build.dart'; import '../models/build.dart';
import 'api_client.dart'; import 'api_client.dart';
extension BuildsAPI on ApiClient { extension BuildsAPI on ApiClient {

View File

@ -1,4 +1,4 @@
import '../core/models/package.dart'; import '../models/package.dart';
import 'api_client.dart'; import 'api_client.dart';
extension PackagesAPI on ApiClient { extension PackagesAPI on ApiClient {
@ -15,8 +15,8 @@ extension PackagesAPI on ApiClient {
} }
Future<void> addPackage({bool force = false, required String name}) async { Future<void> addPackage({bool force = false, required String name}) async {
final resp = await getRawClient().post("/packages/add", data: {'force_build': force, 'name': name}); final resp = await getRawClient()
.post("/packages/add", data: {'force_build': force, 'name': name});
print(resp.data); print(resp.data);
} }
} }

View File

@ -0,0 +1,9 @@
import '../models/stats.dart';
import 'api_client.dart';
extension StatsAPI on ApiClient {
Future<Stats> listStats() async {
final resp = await getRawClient().get("/stats");
return Stats.fromJson(resp.data);
}
}

View File

@ -1,16 +1,21 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Chart extends StatefulWidget { class BuildsChart extends StatefulWidget {
const Chart({ const BuildsChart({
Key? key, Key? key,
required this.nrbuilds,
required this.nrfailedbuilds,
}) : super(key: key); }) : super(key: key);
final int nrbuilds;
final int nrfailedbuilds;
@override @override
_ChartState createState() => _ChartState(); _BuildsChartState createState() => _BuildsChartState();
} }
class _ChartState extends State<Chart> { class _BuildsChartState extends State<BuildsChart> {
int touchedIndex = -1; int touchedIndex = -1;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,9 +36,6 @@ class _ChartState extends State<Chart> {
pieTouchData: PieTouchData( pieTouchData: PieTouchData(
touchCallback: (pieTouchResponse, touchresponse) { touchCallback: (pieTouchResponse, touchresponse) {
setState(() { setState(() {
// final desiredTouch = pieTouchResponse.touchInput
// is! PointerExitEvent &&
// pieTouchResponse.touchInput is! PointerUpEvent;
if (touchresponse?.touchedSection != null) { if (touchresponse?.touchedSection != null) {
touchedIndex = touchresponse! touchedIndex = touchresponse!
.touchedSection!.touchedSectionIndex; .touchedSection!.touchedSectionIndex;
@ -69,8 +71,9 @@ class _ChartState extends State<Chart> {
case 0: case 0:
return PieChartSectionData( return PieChartSectionData(
color: const Color(0xff760707), color: const Color(0xff760707),
value: 40, value: widget.nrfailedbuilds.toDouble(),
title: '28.3%', title:
"${(widget.nrfailedbuilds * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
radius: radius, radius: radius,
titleStyle: TextStyle( titleStyle: TextStyle(
fontSize: fontSize, fontSize: fontSize,
@ -80,8 +83,9 @@ class _ChartState extends State<Chart> {
case 1: case 1:
return PieChartSectionData( return PieChartSectionData(
color: const Color(0xff0a7005), color: const Color(0xff0a7005),
value: 30, value: (widget.nrbuilds - widget.nrfailedbuilds).toDouble(),
title: '16.7%', title:
"${((widget.nrbuilds - widget.nrfailedbuilds) * 100 / widget.nrbuilds).toStringAsFixed(2)}%",
radius: radius, radius: radius,
titleStyle: TextStyle( titleStyle: TextStyle(
fontSize: fontSize, fontSize: fontSize,

View File

@ -1,25 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/constants/color_constants.dart'; import '../../constants/color_constants.dart';
class UserDetailsMiniCard extends StatelessWidget { class ChartCard extends StatelessWidget {
const UserDetailsMiniCard({ const ChartCard({
Key? key, Key? key,
required this.title, required this.title,
required this.color, required this.color,
required this.amountOfFiles, required this.textRight,
required this.numberOfIncrease, required this.subtitle,
}) : super(key: key); }) : super(key: key);
final Color color; final Color color;
final String title, amountOfFiles; final String title, textRight, subtitle;
final int numberOfIncrease;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
margin: EdgeInsets.only(top: defaultPadding), margin: const EdgeInsets.only(top: defaultPadding),
padding: EdgeInsets.all(defaultPadding), padding: const EdgeInsets.all(defaultPadding),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(width: 2, color: primaryColor.withOpacity(0.15)), border: Border.all(width: 2, color: primaryColor.withOpacity(0.15)),
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
@ -46,17 +45,17 @@ class UserDetailsMiniCard extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(
"$numberOfIncrease", subtitle,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.caption! .bodySmall!
.copyWith(color: Colors.white70), .copyWith(color: Colors.white70),
), ),
], ],
), ),
), ),
), ),
Text(amountOfFiles) Text(textRight)
], ],
), ),
); );

View File

@ -0,0 +1,59 @@
import 'package:aurcache/components/dashboard/search_field.dart';
import 'package:flutter/material.dart';
import '../../constants/color_constants.dart';
import '../../utils/responsive.dart';
class Header extends StatelessWidget {
const Header({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
if (!Responsive.isDesktop(context))
IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
),
if (!Responsive.isMobile(context))
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Hello, Arch User 👋",
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(
height: 8,
),
Text(
"Welcome to your Build server",
style: Theme.of(context).textTheme.subtitle2,
),
],
),
if (!Responsive.isMobile(context))
Spacer(flex: Responsive.isDesktop(context) ? 2 : 1),
Expanded(child: SearchField()),
ElevatedButton.icon(
style: TextButton.styleFrom(
backgroundColor: darkgreenColor,
padding: EdgeInsets.symmetric(
horizontal: defaultPadding * 1.5,
vertical: defaultPadding / (Responsive.isMobile(context) ? 2 : 1),
),
),
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text(
"Add New",
),
),
//ProfileCard()
],
);
}
}

View File

@ -0,0 +1,83 @@
import 'package:aurcache/components/dashboard/quick_info_tile.dart';
import 'package:aurcache/utils/file_formatter.dart';
import 'package:flutter/material.dart';
import '../../constants/color_constants.dart';
import '../../models/quick_info_data.dart';
import '../../utils/responsive.dart';
import '../../models/stats.dart';
class QuickInfoBanner extends StatelessWidget {
const QuickInfoBanner({
Key? key,
required this.stats,
}) : super(key: key);
final Stats stats;
@override
Widget build(BuildContext context) {
final Size _size = MediaQuery.of(context).size;
return Column(
children: [
const SizedBox(height: defaultPadding),
Responsive(
mobile: _buildBanner(
crossAxisCount: _size.width < 650 ? 2 : 4,
childAspectRatio: _size.width < 650 ? 1.2 : 1,
),
tablet: _buildBanner(),
desktop: _buildBanner(
childAspectRatio: _size.width < 1400 ? 2.75 : 2.75,
),
),
],
);
}
List<QuickInfoData> buildQuickInfoData() {
return [
QuickInfoData(
color: primaryColor,
icon: Icons.widgets,
title: "Total Packages",
subtitle: stats.total_packages.toString()),
QuickInfoData(
color: const Color(0xFFFFA113),
icon: Icons.hourglass_top,
title: "Active Builds",
subtitle: stats.active_builds.toString()),
QuickInfoData(
color: const Color(0xFFA4CDFF),
icon: Icons.build,
title: "Total Builds",
subtitle: stats.total_builds.toString()),
QuickInfoData(
color: const Color(0xFFd50000),
icon: Icons.storage,
title: "Repo Size",
subtitle: stats.repo_storage_size.readableFileSize()),
QuickInfoData(
color: const Color(0xFF00F260),
icon: Icons.timelapse,
title: "Average Build Time",
subtitle: stats.avg_build_time.toString()),
];
}
Widget _buildBanner({int crossAxisCount = 5, double childAspectRatio = 1}) {
final quickInfo = buildQuickInfoData();
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: quickInfo.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: defaultPadding,
mainAxisSpacing: defaultPadding,
childAspectRatio: childAspectRatio,
),
itemBuilder: (context, idx) => QuickInfoTile(data: quickInfo[idx]),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:aurcache/models/quick_info_data.dart';
import 'package:flutter/material.dart';
import '../../constants/color_constants.dart';
class QuickInfoTile extends StatefulWidget {
const QuickInfoTile({Key? key, required this.data}) : super(key: key);
final QuickInfoData data;
@override
_QuickInfoTileState createState() => _QuickInfoTileState();
}
class _QuickInfoTileState extends State<QuickInfoTile> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(defaultPadding),
decoration: const BoxDecoration(
color: secondaryColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(defaultPadding * 0.75),
height: 64,
width: 64,
decoration: BoxDecoration(
color: widget.data.color.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
child: Icon(
widget.data.icon,
color: widget.data.color,
size: 32,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
widget.data.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
),
Text(
widget.data.subtitle,
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(color: Colors.white70),
),
],
),
],
),
);
}
}

View File

@ -1,13 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:aurcache/api/builds.dart'; import 'package:aurcache/api/builds.dart';
import 'package:aurcache/core/models/build.dart'; import 'package:aurcache/models/build.dart';
import 'package:aurcache/screens/dashboard/components/your_packages.dart'; import 'package:aurcache/components/dashboard/your_packages.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../api/API.dart'; import '../../api/API.dart';
import '../../../core/constants/color_constants.dart'; import '../../constants/color_constants.dart';
import '../../../core/models/package.dart';
class RecentBuilds extends StatefulWidget { class RecentBuilds extends StatefulWidget {
const RecentBuilds({ const RecentBuilds({
@ -78,7 +77,9 @@ class _RecentBuildsState extends State<RecentBuilds> {
label: Text("Status"), label: Text("Status"),
), ),
], ],
rows: snapshot.data!.map((e) => recentUserDataRow(e)).toList(), rows: snapshot.data!
.map((e) => recentUserDataRow(e))
.toList(),
); );
} else { } else {
return Text("no data"); return Text("no data");

View File

@ -0,0 +1,47 @@
import 'package:aurcache/api/packages.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../api/API.dart';
import '../../constants/color_constants.dart';
class SearchField extends StatelessWidget {
SearchField({
Key? key,
}) : super(key: key);
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
hintText: "Search",
fillColor: secondaryColor,
filled: true,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
suffixIcon: InkWell(
onTap: () {
// todo this is only temporary -> add this to a proper page
API.addPackage(name: controller.text);
},
child: Container(
padding: EdgeInsets.all(defaultPadding * 0.75),
margin: EdgeInsets.symmetric(horizontal: defaultPadding / 2),
decoration: const BoxDecoration(
color: darkgreenColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: SvgPicture.asset(
"assets/icons/Search.svg",
),
),
),
),
);
}
}

View File

@ -1,14 +1,19 @@
import 'package:aurcache/screens/dashboard/components/chart_card.dart'; import 'package:aurcache/components/dashboard/chart_card.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/constants/color_constants.dart'; import '../../constants/color_constants.dart';
import 'charts.dart'; import 'builds_chart.dart';
class UserDetailsWidget extends StatelessWidget { class SidePanel extends StatelessWidget {
const UserDetailsWidget({ const SidePanel({
Key? key, Key? key,
required this.nrbuilds,
required this.nrfailedbuilds,
}) : super(key: key); }) : super(key: key);
final int nrbuilds;
final int nrfailedbuilds;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -20,26 +25,28 @@ class UserDetailsWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( const Text(
"Package build success", "Package build success",
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
SizedBox(height: defaultPadding), const SizedBox(height: defaultPadding),
Chart(), BuildsChart(nrbuilds: nrbuilds, nrfailedbuilds: nrfailedbuilds),
UserDetailsMiniCard( ChartCard(
color: const Color(0xff0a7005), color: const Color(0xff0a7005),
title: "Successful Builds", title: "Successful Builds",
amountOfFiles: "%16.7", textRight:
numberOfIncrease: 1328, "${((nrbuilds - nrfailedbuilds) * 100 / nrbuilds).toStringAsFixed(2)}%",
subtitle: (nrbuilds - nrfailedbuilds).toString(),
), ),
UserDetailsMiniCard( ChartCard(
color: const Color(0xff760707), color: const Color(0xff760707),
title: "Failed Builds", title: "Failed Builds",
amountOfFiles: "%28.3", textRight:
numberOfIncrease: 1328, "${(nrfailedbuilds * 100 / nrbuilds).toStringAsFixed(2)}%",
subtitle: nrfailedbuilds.toString(),
), ),
], ],
), ),

View File

@ -3,9 +3,9 @@ import 'dart:async';
import 'package:aurcache/api/packages.dart'; import 'package:aurcache/api/packages.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../api/API.dart'; import '../../api/API.dart';
import '../../../core/constants/color_constants.dart'; import '../../constants/color_constants.dart';
import '../../../core/models/package.dart'; import '../../models/package.dart';
class YourPackages extends StatefulWidget { class YourPackages extends StatefulWidget {
const YourPackages({ const YourPackages({

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import '../../../core/constants/color_constants.dart'; import '../constants/color_constants.dart';
class SideMenu extends StatelessWidget { class SideMenu extends StatelessWidget {
const SideMenu({ const SideMenu({

View File

@ -6,7 +6,7 @@ const primaryColor = Color(0xFF2697FF);
const secondaryColor = Color(0xFF292929); const secondaryColor = Color(0xFF292929);
const bgColor = Color(0xFF212121); const bgColor = Color(0xFF212121);
const darkgreenColor = Color(0xFF2c614f); const darkgreenColor = Color(0xff0a7005);
const greenColor = Color(0xFF6bab58); const greenColor = Color(0xFF6bab58);
const defaultPadding = 16.0; const defaultPadding = 16.0;

View File

@ -1,8 +1,8 @@
import 'package:aurcache/screens/home/home_screen.dart'; import 'package:aurcache/screens/home_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'core/constants/color_constants.dart'; import 'constants/color_constants.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());

View File

@ -5,7 +5,9 @@ class Build {
final int status; final int status;
Build( Build(
{required this.id,required this.pkg_name, required this.version, {required this.id,
required this.pkg_name,
required this.version,
required this.status}); required this.status});
factory Build.fromJson(Map<String, dynamic> json) { factory Build.fromJson(Map<String, dynamic> json) {

View File

@ -1,289 +0,0 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../core/constants/color_constants.dart';
class DailyInfoModel {
IconData? icon;
String? title;
String? totalStorage;
int? volumeData;
int? percentage;
Color? color;
List<Color>? colors;
List<FlSpot>? spots;
DailyInfoModel({
this.icon,
this.title,
this.totalStorage,
this.volumeData,
this.percentage,
this.color,
this.colors,
this.spots,
});
DailyInfoModel.fromJson(Map<String, dynamic> json) {
title = json['title'];
volumeData = json['volumeData'];
icon = json['icon'];
totalStorage = json['totalStorage'];
color = json['color'];
percentage = json['percentage'];
colors = json['colors'];
spots = json['spots'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['title'] = this.title;
data['volumeData'] = this.volumeData;
data['icon'] = this.icon;
data['totalStorage'] = this.totalStorage;
data['color'] = this.color;
data['percentage'] = this.percentage;
data['colors'] = this.colors;
data['spots'] = this.spots;
return data;
}
}
List<DailyInfoModel> dailyDatas =
dailyData.map((item) => DailyInfoModel.fromJson(item)).toList();
//List<FlSpot> spots = yValues.asMap().entries.map((e) {
// return FlSpot(e.key.toDouble(), e.value);
//}).toList();
var dailyData = [
{
"title": "Employee",
"volumeData": 1328,
"icon": Icons.ac_unit,
"totalStorage": "+ %20",
"color": primaryColor,
"percentage": 35,
"colors": [
Color(0xff23b6e6),
Color(0xff02d39a),
],
"spots": [
FlSpot(
1,
2,
),
FlSpot(
2,
1.0,
),
FlSpot(
3,
1.8,
),
FlSpot(
4,
1.5,
),
FlSpot(
5,
1.0,
),
FlSpot(
6,
2.2,
),
FlSpot(
7,
1.8,
),
FlSpot(
8,
1.5,
)
]
},
{
"title": "On Leave",
"volumeData": 1328,
"icon": Icons.ac_unit,
"totalStorage": "+ %5",
"color": Color(0xFFFFA113),
"percentage": 35,
"colors": [Color(0xfff12711), Color(0xfff5af19)],
"spots": [
FlSpot(
1,
1.3,
),
FlSpot(
2,
1.0,
),
FlSpot(
3,
4,
),
FlSpot(
4,
1.5,
),
FlSpot(
5,
1.0,
),
FlSpot(
6,
3,
),
FlSpot(
7,
1.8,
),
FlSpot(
8,
1.5,
)
]
},
{
"title": "Onboarding",
"volumeData": 1328,
"icon": Icons.ac_unit,
"totalStorage": "+ %8",
"color": Color(0xFFA4CDFF),
"percentage": 10,
"colors": [Color(0xff2980B9), Color(0xff6DD5FA)],
"spots": [
FlSpot(
1,
1.3,
),
FlSpot(
2,
5,
),
FlSpot(
3,
1.8,
),
FlSpot(
4,
6,
),
FlSpot(
5,
1.0,
),
FlSpot(
6,
2.2,
),
FlSpot(
7,
1.8,
),
FlSpot(
8,
1,
)
]
},
{
"title": "Open Position",
"volumeData": 1328,
"icon": Icons.ac_unit,
"totalStorage": "+ %8",
"color": Color(0xFFd50000),
"percentage": 10,
"colors": [Color(0xff93291E), Color(0xffED213A)],
"spots": [
FlSpot(
1,
3,
),
FlSpot(
2,
4,
),
FlSpot(
3,
1.8,
),
FlSpot(
4,
1.5,
),
FlSpot(
5,
1.0,
),
FlSpot(
6,
2.2,
),
FlSpot(
7,
1.8,
),
FlSpot(
8,
1.5,
)
]
},
{
"title": "Efficiency",
"volumeData": 5328,
"icon": Icons.ac_unit,
"totalStorage": "- %5",
"color": Color(0xFF00F260),
"percentage": 78,
"colors": [Color(0xff0575E6), Color(0xff00F260)],
"spots": [
FlSpot(
1,
1.3,
),
FlSpot(
2,
1.0,
),
FlSpot(
3,
1.8,
),
FlSpot(
4,
1.5,
),
FlSpot(
5,
1.0,
),
FlSpot(
6,
2.2,
),
FlSpot(
7,
1.8,
),
FlSpot(
8,
1.5,
)
]
}
];
//final List<double> yValues = [
// 2.3,
// 1.8,
// 1.9,
// 1.5,
// 1.0,
// 2.2,
// 1.8,
// 1.5,
//];

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class QuickInfoData {
const QuickInfoData({
Key? key,
required this.color,
required this.icon,
required this.title,
required this.subtitle,
});
final Color color;
final IconData icon;
final String title, subtitle;
}

View File

@ -1,64 +0,0 @@
class SliderModel {
String? image;
String? text;
String? altText;
String? bAltText;
String? productImage;
int? kBackgroundColor;
SliderModel(this.image, this.text, this.altText, this.bAltText,
this.productImage, this.kBackgroundColor);
SliderModel.fromJson(Map<String, dynamic> json) {
image = json['image'];
kBackgroundColor = json['kBackgroundColor'];
text = json['text'];
altText = json['altText'];
bAltText = json['bAltText'];
productImage = json['productImage'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['image'] = this.image;
data['kBackgroundColor'] = this.kBackgroundColor;
data['text'] = this.text;
data['altText'] = this.altText;
data['bAltText'] = this.bAltText;
data['productImage'] = this.productImage;
return data;
}
}
List<SliderModel> slides =
slideData.map((item) => SliderModel.fromJson(item)).toList();
var slideData = [
{
"image": "assets/slides/background-1.jpeg",
"kBackgroundColor": 0xFF2c614f,
"text": "Welcome to the Smart Smart Admin Dashboard!",
"altText": "You can access & track your services in real-time.",
"bAltText": "Are you ready for the next generation AI supported Dashboard?",
"productImage": "assets/images/mockup.png"
},
{
"image": "assets/slides/background-2.jpeg",
"kBackgroundColor": 0xFF8a1a4c,
"text": "¡Bienvenido al tablero Smart Admin Dashboard!",
"altText": "Puede acceder y rastrear sus servicios en tiempo real.",
"bAltText":
"¿Estás listo para el panel de control impulsado por IA de próxima generación?",
"productImage": "assets/images/mockup-2.png"
},
{
"image": "assets/slides/background-3.jpeg",
"kBackgroundColor": 0xFF0ab3ec,
"text": "Willkommen im Smart Admin Dashboard!",
"altText":
"Sie können in Echtzeit auf Ihre Dienste zugreifen und diese verfolgen.",
"bAltText":
"Sind Sie bereit für das AI-unterstützte Dashboard der nächsten Generation?",
"productImage": "assets/images/mockup-3.png"
}
];

View File

@ -0,0 +1,30 @@
class Stats {
final int total_builds,
failed_builds,
avg_queue_wait_time,
avg_build_time,
repo_storage_size,
active_builds,
total_packages;
factory Stats.fromJson(Map<String, dynamic> json) {
return Stats(
total_builds: json["total_builds"] as int,
failed_builds: json["failed_builds"] as int,
avg_queue_wait_time: json["avg_queue_wait_time"] as int,
avg_build_time: json["avg_build_time"] as int,
repo_storage_size: json["repo_storage_size"] as int,
active_builds: json["active_builds"] as int,
total_packages: json["total_packages"] as int,
);
}
Stats(
{required this.total_builds,
required this.failed_builds,
required this.avg_queue_wait_time,
required this.avg_build_time,
required this.repo_storage_size,
required this.active_builds,
required this.total_packages});
}

View File

@ -1,125 +0,0 @@
import 'package:aurcache/api/packages.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../api/API.dart';
import '../../../core/constants/color_constants.dart';
import '../../../responsive.dart';
class Header extends StatelessWidget {
const Header({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
if (!Responsive.isDesktop(context))
IconButton(
icon: Icon(Icons.menu),
onPressed: () {},
),
if (!Responsive.isMobile(context))
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Hello, Arch User 👋",
style: Theme.of(context).textTheme.headline6,
),
SizedBox(
height: 8,
),
Text(
"Welcome to your Build server",
style: Theme.of(context).textTheme.subtitle2,
),
],
),
if (!Responsive.isMobile(context))
Spacer(flex: Responsive.isDesktop(context) ? 2 : 1),
Expanded(child: SearchField()),
//ProfileCard()
],
);
}
}
class ProfileCard extends StatelessWidget {
const ProfileCard({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: defaultPadding),
padding: EdgeInsets.symmetric(
horizontal: defaultPadding,
vertical: defaultPadding / 2,
),
decoration: BoxDecoration(
color: secondaryColor,
borderRadius: const BorderRadius.all(Radius.circular(10)),
border: Border.all(color: Colors.white10),
),
child: Row(
children: [
CircleAvatar(
backgroundImage: AssetImage("assets/images/profile_pic.png"),
),
if (!Responsive.isMobile(context))
Padding(
padding:
const EdgeInsets.symmetric(horizontal: defaultPadding / 2),
child: Text("Deniz Çolak"),
),
Icon(Icons.keyboard_arrow_down),
],
),
);
}
}
class SearchField extends StatelessWidget {
SearchField({
Key? key,
}) : super(key: key);
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
hintText: "Search",
fillColor: secondaryColor,
filled: true,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
suffixIcon: InkWell(
onTap: () {
// todo this is only temporary -> add this to a proper page
API.addPackage(name: controller.text);
},
child: Container(
padding: EdgeInsets.all(defaultPadding * 0.75),
margin: EdgeInsets.symmetric(horizontal: defaultPadding / 2),
decoration: const BoxDecoration(
color: greenColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: SvgPicture.asset(
"assets/icons/Search.svg",
),
),
),
),
);
}
}

View File

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import '../../../core/constants/color_constants.dart';
import '../../../models/daily_info_model.dart';
import '../../../responsive.dart';
import 'mini_information_widget.dart';
class MiniInformation extends StatelessWidget {
const MiniInformation({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final Size _size = MediaQuery.of(context).size;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 10,
),
ElevatedButton.icon(
style: TextButton.styleFrom(
backgroundColor: Colors.green,
padding: EdgeInsets.symmetric(
horizontal: defaultPadding * 1.5,
vertical:
defaultPadding / (Responsive.isMobile(context) ? 2 : 1),
),
),
onPressed: () {},
icon: Icon(Icons.add),
label: Text(
"Add New",
),
),
],
),
SizedBox(height: defaultPadding),
Responsive(
mobile: InformationCard(
crossAxisCount: _size.width < 650 ? 2 : 4,
childAspectRatio: _size.width < 650 ? 1.2 : 1,
),
tablet: InformationCard(),
desktop: InformationCard(
childAspectRatio: _size.width < 1400 ? 1.2 : 1.4,
),
),
],
);
}
}
class InformationCard extends StatelessWidget {
const InformationCard({
Key? key,
this.crossAxisCount = 5,
this.childAspectRatio = 1,
}) : super(key: key);
final int crossAxisCount;
final double childAspectRatio;
@override
Widget build(BuildContext context) {
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: dailyDatas.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: defaultPadding,
mainAxisSpacing: defaultPadding,
childAspectRatio: childAspectRatio,
),
itemBuilder: (context, index) =>
MiniInformationWidget(dailyData: dailyDatas[index]),
);
}
}

View File

@ -1,206 +0,0 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../../../core/constants/color_constants.dart';
import '../../../models/daily_info_model.dart';
class MiniInformationWidget extends StatefulWidget {
const MiniInformationWidget({
Key? key,
required this.dailyData,
}) : super(key: key);
final DailyInfoModel dailyData;
@override
_MiniInformationWidgetState createState() => _MiniInformationWidgetState();
}
int _value = 1;
class _MiniInformationWidgetState extends State<MiniInformationWidget> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(defaultPadding),
decoration: const BoxDecoration(
color: secondaryColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: EdgeInsets.all(defaultPadding * 0.75),
height: 40,
width: 40,
decoration: BoxDecoration(
color: widget.dailyData.color!.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
child: Icon(
widget.dailyData.icon,
color: widget.dailyData.color,
size: 18,
),
),
Padding(
padding: EdgeInsets.only(right: 12.0),
child: DropdownButton(
icon: Icon(Icons.more_vert, size: 18),
underline: SizedBox(),
style: Theme.of(context).textTheme.button,
value: _value,
items: [
DropdownMenuItem(
child: Text("Daily"),
value: 1,
),
DropdownMenuItem(
child: Text("Weekly"),
value: 2,
),
DropdownMenuItem(
child: Text("Monthly"),
value: 3,
),
],
onChanged: (int? value) {
setState(() {
_value = value!;
});
},
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.dailyData.title!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 8,
),
Container(
child: LineChartWidget(
colors: widget.dailyData.colors,
spotsData: widget.dailyData.spots,
),
)
],
),
SizedBox(
height: 8,
),
ProgressLine(
color: widget.dailyData.color!,
percentage: widget.dailyData.percentage!,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${widget.dailyData.volumeData}",
style: Theme.of(context)
.textTheme
.caption!
.copyWith(color: Colors.white70),
),
Text(
widget.dailyData.totalStorage!,
style: Theme.of(context)
.textTheme
.caption!
.copyWith(color: Colors.white),
),
],
)
],
),
);
}
}
class LineChartWidget extends StatelessWidget {
const LineChartWidget({
Key? key,
required this.colors,
required this.spotsData,
}) : super(key: key);
final List<Color>? colors;
final List<FlSpot>? spotsData;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
width: 80,
height: 30,
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: spotsData!,
belowBarData: BarAreaData(show: false),
aboveBarData: BarAreaData(show: false),
isCurved: true,
dotData: FlDotData(show: false),
//colors: colors,
barWidth: 3),
],
lineTouchData: LineTouchData(enabled: false),
titlesData: FlTitlesData(show: false),
//axisTitleData: FlAxisTitleData(show: false),
gridData: FlGridData(show: false),
borderData: FlBorderData(show: false)),
),
),
],
);
}
}
class ProgressLine extends StatelessWidget {
const ProgressLine({
Key? key,
this.color = primaryColor,
required this.percentage,
}) : super(key: key);
final Color color;
final int percentage;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
width: double.infinity,
height: 5,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
LayoutBuilder(
builder: (context, constraints) => Container(
width: constraints.maxWidth * (percentage / 100),
height: 5,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
],
);
}
}

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import '../../core/constants/color_constants.dart';
import '../../responsive.dart';
import 'components/header.dart';
import 'components/mini_information_card.dart';
import 'components/recent_builds.dart';
import 'components/your_packages.dart';
import 'components/user_details_widget.dart';
class DashboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
//padding: EdgeInsets.all(defaultPadding),
child: Container(
padding: EdgeInsets.all(defaultPadding),
child: Column(
children: [
Header(),
SizedBox(height: defaultPadding),
MiniInformation(),
SizedBox(height: defaultPadding),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 5,
child: Column(
children: [
YourPackages(),
SizedBox(height: defaultPadding),
RecentBuilds(),
if (Responsive.isMobile(context))
SizedBox(height: defaultPadding),
if (Responsive.isMobile(context)) UserDetailsWidget(),
],
),
),
if (!Responsive.isMobile(context))
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: UserDetailsWidget(),
),
],
)
],
),
),
),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:aurcache/api/statistics.dart';
import 'package:flutter/material.dart';
import '../api/API.dart';
import '../components/dashboard/header.dart';
import '../constants/color_constants.dart';
import '../utils/responsive.dart';
import '../models/stats.dart';
import '../components/dashboard/quick_info_banner.dart';
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();
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: stats,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
final Stats stats = snapshot.data!;
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: [
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),
],
),
),
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),
),
],
)
],
),
),
),
);
} else {
return const Text("loading");
}
});
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../responsive.dart'; import '../utils/responsive.dart';
import '../dashboard/dashboard_screen.dart'; import 'dashboard_screen.dart';
import 'components/side_menu.dart'; import '../components/side_menu.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});

View File

@ -0,0 +1,15 @@
import 'dart:math';
double _log10(num x) => log(x) / ln10;
extension FileFormatter on num {
String readableFileSize({bool base1024 = true}) {
if (this <= 0) return '0';
final base = base1024 ? 1024 : 1000;
final units = base1024
? ['Bi', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
final digitGroups = (_log10(this) / _log10(base)).floor();
return '${(this / pow(base, digitGroups)).toStringAsFixed(2)} ${units[digitGroups]}';
}
}