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:
parent
ce7a260760
commit
600c2057fe
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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")]
|
||||||
|
17
backend/src/utils/dir_size.rs
Normal file
17
backend/src/utils/dir_size.rs
Normal 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
1
backend/src/utils/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod dir_size;
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
9
frontend/lib/api/statistics.dart
Normal file
9
frontend/lib/api/statistics.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
@ -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)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
59
frontend/lib/components/dashboard/header.dart
Normal file
59
frontend/lib/components/dashboard/header.dart
Normal 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()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
83
frontend/lib/components/dashboard/quick_info_banner.dart
Normal file
83
frontend/lib/components/dashboard/quick_info_banner.dart
Normal 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]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
frontend/lib/components/dashboard/quick_info_tile.dart
Normal file
65
frontend/lib/components/dashboard/quick_info_tile.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
47
frontend/lib/components/dashboard/search_field.dart
Normal file
47
frontend/lib/components/dashboard/search_field.dart
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
@ -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({
|
@ -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({
|
@ -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;
|
@ -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());
|
||||||
|
@ -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) {
|
@ -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,
|
|
||||||
//];
|
|
15
frontend/lib/models/quick_info_data.dart
Normal file
15
frontend/lib/models/quick_info_data.dart
Normal 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;
|
||||||
|
}
|
@ -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"
|
|
||||||
}
|
|
||||||
];
|
|
30
frontend/lib/models/stats.dart
Normal file
30
frontend/lib/models/stats.dart
Normal 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});
|
||||||
|
}
|
@ -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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
78
frontend/lib/screens/dashboard_screen.dart
Normal file
78
frontend/lib/screens/dashboard_screen.dart
Normal 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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});
|
15
frontend/lib/utils/file_formatter.dart
Normal file
15
frontend/lib/utils/file_formatter.dart
Normal 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]}';
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user