diff --git a/webgui/webapp/src/app/app.component.html b/webgui/webapp/src/app/app.component.html index f544a72..b81433f 100644 --- a/webgui/webapp/src/app/app.component.html +++ b/webgui/webapp/src/app/app.component.html @@ -72,7 +72,7 @@

DATA PORTAL

diff --git a/webgui/webapp/src/app/app.component.scss b/webgui/webapp/src/app/app.component.scss index c88dc7c..6417fa3 100644 --- a/webgui/webapp/src/app/app.component.scss +++ b/webgui/webapp/src/app/app.component.scss @@ -5,7 +5,7 @@ .bui-header-wrapper { overflow: hidden; background: url(/assets/images/header.png) no-repeat right top; - background-size: contain; + background-size: cover; @media (max-width: 1024px) { background-size: cover; @@ -144,7 +144,8 @@ .bui-footer { height: 10%; overflow: hidden; - background: #2f2f2f url(/assets/images/bg02.jpg) repeat; + // background: #2f2f2f url(/assets/images/bg02.jpg) repeat; + background-color: theme("colors.primary-10"); padding: 50px 1px; text-align: center; color: #6f6f6f; @@ -179,7 +180,6 @@ text-transform: uppercase; font-weight: 400; font-size: 0.9em; - color: #6f6f6f; } .bui-byline { @@ -187,7 +187,6 @@ top: -2em; font-weight: 500; font-size: 1em; - color: #8e8e8e; font: 16px Arial; } @@ -206,6 +205,10 @@ transition: transform 0.4s; } +.bui-menu-items .active a { + color: theme("colors.warning-10"); +} + .bui-menu-item.active { transform: scale(1.2); transition: transform 0.4s; diff --git a/webgui/webapp/src/app/pages/admin-page/admin-page.component.ts b/webgui/webapp/src/app/pages/admin-page/admin-page.component.ts index 742c17e..902e5ba 100644 --- a/webgui/webapp/src/app/pages/admin-page/admin-page.component.ts +++ b/webgui/webapp/src/app/pages/admin-page/admin-page.component.ts @@ -156,22 +156,23 @@ export class AdminPageComponent implements OnInit { }); } + async openDialog(row: any, res: any) {} + async userClick(row: any) { const { AdminUserClickDialogComponent } = await import( 'src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component' ); + const dialog = this.dg.open(AdminUserClickDialogComponent, { data: { + sub: row.Sub, name: `${row['First name']} ${row['Last name']}`, email: row.Email, firstName: `${row['First name']}`, lastName: `${row['Last name']}`, - // TODO: Add more user attributes - sizeOfData: 0, - countOfQueries: 0, - costEstimation: 0, }, }); + dialog.afterClosed().subscribe((data) => { if (_.get(data, 'reload', false)) { this.resetPagination(); @@ -256,6 +257,7 @@ export class AdminPageComponent implements OnInit { this.usersTableDataSource = _.map( _.get(response, 'users', []), (user: any) => ({ + Sub: _.get(_.find(user.Attributes, { Name: 'sub' }), 'Value', ''), Email: _.get( _.find(user.Attributes, { Name: 'email' }), 'Value', diff --git a/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.html b/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.html index 44f0a14..1deb903 100644 --- a/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.html +++ b/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.html @@ -43,13 +43,13 @@

Create New User

Quota
Size of Data - + GB Field is required Count of Queries - + Count Field is required @@ -106,7 +106,7 @@

Create New User

mat-raised-button color="primary" (click)="createUser()" - [disabled]="newUserForm.invalid" + [disabled]="newUserForm.invalid || !costEstimation" > Create User diff --git a/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.ts b/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.ts index c463360..8117cee 100644 --- a/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.ts +++ b/webgui/webapp/src/app/pages/admin-page/components/admin-create-user-dialog/admin-create-user-dialog.component.ts @@ -31,6 +31,8 @@ import { MatInputModule } from '@angular/material/input'; import { SpinnerService } from 'src/app/services/spinner.service'; import { MatSnackBar } from '@angular/material/snack-bar'; import { AwsService } from 'src/app/services/aws.service'; +import { gigabytesToBytes } from 'src/app/utils/file'; +import { UserQuotaService } from 'src/app/services/userquota.service'; @Component({ selector: 'app-admin-create-user-dialog', @@ -69,6 +71,7 @@ export class AdminCreateUserComponent implements OnInit { private adminServ: AdminService, private sb: MatSnackBar, private aws: AwsService, + private uq: UserQuotaService, ) { this.newUserForm = this.fb.group({ firstName: ['', Validators.required], @@ -76,8 +79,8 @@ export class AdminCreateUserComponent implements OnInit { email: ['', [Validators.required, Validators.email]], administrators: [false], // Quota - sizeOfData: ['', [Validators.required, Validators.min(0)]], - countOfQueries: ['', [Validators.required, Validators.min(0)]], + quotaSize: ['', [Validators.required, Validators.min(0)]], + quotaQueryCount: ['', [Validators.required, Validators.min(0)]], }); } @@ -89,11 +92,11 @@ export class AdminCreateUserComponent implements OnInit { this.newUserForm.valueChanges .pipe(debounceTime(400), distinctUntilChanged()) .subscribe((values) => { - if (values.countOfQueries && values.sizeOfData) { + if (values.quotaQueryCount && values.quotaSize) { this.aws .calculateQuotaEstimationPerMonth( - values.countOfQueries, - values.sizeOfData, + values.quotaQueryCount, + values.quotaSize, ) .subscribe((res) => { @@ -149,8 +152,9 @@ export class AdminCreateUserComponent implements OnInit { .subscribe((response) => { //api response always null this.ss.end(); - if (response) { + this.addUserQuota(response.uid); + this.newUserForm.reset(); this.sb.open('User created successfully!', 'Okay', { duration: 60000, @@ -160,6 +164,17 @@ export class AdminCreateUserComponent implements OnInit { }); } + addUserQuota(sub: string): void { + this.uq + .upsertUserQuota(sub, this.costEstimation, { + quotaSize: gigabytesToBytes(this.newUserForm.value.quotaSize), + quotaQueryCount: this.newUserForm.value.quotaQueryCount, + usageSize: 0, + usageCount: 0, + }) + .pipe(catchError(() => of(null))); + } + updateUserRole(email: string, isAdmin: boolean): void { this.as .updateUsersGroups(email, { administrators: isAdmin }) diff --git a/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.html b/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.html index e4d6db3..289f31b 100644 --- a/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.html +++ b/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.html @@ -3,7 +3,7 @@

Update {{ data.name }}

-
+
User Info
@@ -42,7 +42,7 @@

Update {{ data.name }}

Valid email is required
-
+
Update {{ data.name }}
Quota
Size of Data - + GB Field is required Count of Queries - + Count Field is required
Estimated Cost* : - ${{ data.costEstimation }} / Month + ${{ costEstimation }} / Month
@@ -129,6 +122,7 @@

Update {{ data.name }}

mat-raised-button color="primary" (click)="done()" + [disabled]="form.invalid || !costEstimation" > Update diff --git a/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.ts b/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.ts index da0cdd2..747f6f3 100644 --- a/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.ts +++ b/webgui/webapp/src/app/pages/admin-page/components/admin-user-click-dialog/admin-user-click-dialog.component.ts @@ -14,13 +14,23 @@ import { FormGroup, FormsModule, ReactiveFormsModule, + Validators, } from '@angular/forms'; import { AdminService } from 'src/app/pages/admin-page/services/admin.service'; -import { catchError, of } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + forkJoin, + of, +} from 'rxjs'; import * as _ from 'lodash'; import { ComponentSpinnerComponent } from 'src/app/components/component-spinner/component-spinner.component'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { AwsService } from 'src/app/services/aws.service'; +import { bytesToGigabytes, gigabytesToBytes } from 'src/app/utils/file'; +import { UserQuotaService } from 'src/app/services/userquota.service'; @Component({ selector: 'app-admin-user-click-dialog', @@ -48,44 +58,108 @@ export class AdminUserClickDialogComponent implements OnInit { protected loading = false; protected disableDelete = false; + // quota + protected costEstimation: number | null = 0; + protected usageSize = 0; + protected usageCount = 0; + constructor( public dialogRef: MatDialogRef, private fb: FormBuilder, private as: AdminService, + private uq: UserQuotaService, + private aws: AwsService, private dg: MatDialog, @Inject(MAT_DIALOG_DATA) public data: any, ) { this.form = this.fb.group({ administrators: [false], - managers: [false], + quotaSize: ['', [Validators.required, Validators.min(0)]], + quotaQueryCount: ['', [Validators.required, Validators.min(0)]], }); } ngOnInit(): void { + this.dialogRef.afterOpened().subscribe(() => { + this.getUserGroups(); + }); + + this.onChangeCalculateCost(); + } + + onChangeCalculateCost() { + this.form.valueChanges + .pipe(debounceTime(400), distinctUntilChanged()) + .subscribe((values) => { + if (values.quotaQueryCount && values.quotaSize) { + this.aws + .calculateQuotaEstimationPerMonth( + values.quotaQueryCount, + values.quotaSize, + ) + + .subscribe((res) => { + this.costEstimation = res; + }); + } + }); + } + + getUserGroups() { this.loading = true; - this.as + // Define both observables + const userQuota$ = this.uq + .getUserQuota(this.data.sub) + .pipe(catchError(() => of(null))); + + const userGroups$ = this.as .listUsersGroups(this.data.email) - .pipe(catchError(() => of(null))) - .subscribe((response: any) => { - const groups = _.get(response, 'groups', []); - const user = _.get(response, 'user', null); - const authorizer = _.get(response, 'authorizer', null); - const groupNames = _.map( - groups, - (group) => _.split(group.GroupName, '-')[0], - ); - const userGroups: { [key: string]: boolean } = {}; - _.each(groupNames, (gn: string) => { - userGroups[gn] = true; - }); - _.merge(this.initialGroups, userGroups); - this.form.patchValue(userGroups); - if (user === authorizer) { - this.form.get('administrators')?.disable(); - this.disableDelete = true; + .pipe(catchError(() => of(null))); + + // Use forkJoin to run them in parallel + forkJoin({ userQuota: userQuota$, userGroups: userGroups$ }).subscribe( + ({ userQuota, userGroups }) => { + // Process user quota response + if (userQuota) { + this.costEstimation = userQuota.CostEstimation; + this.usageSize = userQuota.Usage.usageSize; + this.usageCount = userQuota.Usage.usageCount; + + this.form.patchValue({ + quotaSize: bytesToGigabytes(userQuota.Usage.quotaSize), + quotaQueryCount: userQuota.Usage.quotaQueryCount, + }); + } + + // Process user groups response + if (userGroups) { + const groups = _.get(userGroups, 'groups', []); + const user = _.get(userGroups, 'user', null); + const authorizer = _.get(userGroups, 'authorizer', null); + const groupNames = _.map( + groups, + (group) => _.split(group.GroupName, '-')[0], + ); + const userGroupsObj: { [key: string]: boolean } = {}; + _.each(groupNames, (gn: string) => { + userGroupsObj[gn] = true; + }); + _.merge(this.initialGroups, userGroupsObj); + this.form.patchValue(userGroupsObj); + + if (user === authorizer) { + this.form.get('administrators')?.disable(); + this.disableDelete = true; + } } + this.loading = false; - }); + }, + (error) => { + console.error('Error loading user data:', error); + this.loading = false; + }, + ); } async delete() { @@ -117,19 +191,33 @@ export class AdminUserClickDialogComponent implements OnInit { this.dialogRef.close(); } - done(): void { - if (_.isEqual(this.initialGroups, this.form.value)) { - this.dialogRef.close(); - return; - } + updateQuota() { + return this.uq + .upsertUserQuota(this.data.sub, this.costEstimation, { + quotaSize: gigabytesToBytes(this.form.value.quotaSize), + quotaQueryCount: this.form.value.quotaQueryCount, + usageSize: this.usageSize, // bytes + usageCount: this.usageCount, + }) + .pipe(catchError(() => of(null))); + } + updateUser() { + const groups = _.pick(this.form.value, ['administrators']); + return this.as + .updateUsersGroups(this.data.email, groups) + .pipe(catchError(() => of(null))); + } + + done(): void { this.loading = true; - this.as - .updateUsersGroups(this.data.email, this.form.value) - .pipe(catchError(() => of(null))) - .subscribe(() => { - this.loading = false; - this.dialogRef.close(); - }); + + const updateQuota$ = this.updateQuota(); + const updateUser$ = this.updateUser(); + + forkJoin([updateQuota$, updateUser$]).subscribe(() => { + this.loading = false; + this.dialogRef.close(); + }); } } diff --git a/webgui/webapp/src/app/pages/clinic-page/svep-results/svep-results.component.html b/webgui/webapp/src/app/pages/clinic-page/svep-results/svep-results.component.html index aca29b6..1917cec 100644 --- a/webgui/webapp/src/app/pages/clinic-page/svep-results/svep-results.component.html +++ b/webgui/webapp/src/app/pages/clinic-page/svep-results/svep-results.component.html @@ -16,6 +16,7 @@

View Results

class="w-[110px] mr-1" mat-raised-button color="primary" + style="color: white" [routerLink]="[]" [queryParams]="{ jobId: requestIdFormControl.value }" >Load -

+

Login

diff --git a/webgui/webapp/src/app/pages/portal-page/query-page/components/query-result-viewer-container/query-result-viewer-container.component.ts b/webgui/webapp/src/app/pages/portal-page/query-page/components/query-result-viewer-container/query-result-viewer-container.component.ts index acf38f2..8c69125 100644 --- a/webgui/webapp/src/app/pages/portal-page/query-page/components/query-result-viewer-container/query-result-viewer-container.component.ts +++ b/webgui/webapp/src/app/pages/portal-page/query-page/components/query-result-viewer-container/query-result-viewer-container.component.ts @@ -11,9 +11,12 @@ import { MatCardModule } from '@angular/material/card'; import { DportalService } from 'src/app/services/dportal.service'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { SpinnerService } from 'src/app/services/spinner.service'; -import { catchError, from, of } from 'rxjs'; +import { catchError, filter, firstValueFrom, from, of, switchMap } from 'rxjs'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { Storage } from 'aws-amplify'; +import { getTotalStorageSize } from 'src/app/utils/file'; +import { UserQuotaService } from 'src/app/services/userquota.service'; +import { AuthService } from 'src/app/services/auth.service'; @Component({ selector: 'app-query-result-viewer-container', @@ -51,6 +54,7 @@ export class QueryResultViewerContainerComponent implements OnChanges { private dg: MatDialog, private ss: SpinnerService, private sb: MatSnackBar, + private uq: UserQuotaService, ) {} ngOnChanges(): void { @@ -69,6 +73,7 @@ export class QueryResultViewerContainerComponent implements OnChanges { type: 'text/json;charset=utf-8;', }); const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = 'data.json'; @@ -77,7 +82,52 @@ export class QueryResultViewerContainerComponent implements OnChanges { document.body.removeChild(a); } + // Calculate total size from storage and current query result + async totalStorage(queryResults: any) { + // Get files in the storage + const res = await Storage.list(``, { + pageSize: 'ALL', + level: 'private', + }); + + // Get total size from storage + const bytesTotal = getTotalStorageSize(res.results); + + // Get size from current query result + const blob = new Blob([JSON.stringify(queryResults, null, 2)], { + type: 'text/json;charset=utf-8;', + }); + + return bytesTotal + blob.size; + } + + updateUserQuota(userQuota: any, currentTotalSize: number) { + this.uq + .upsertUserQuota(userQuota.userSub, userQuota.costEstimation, { + quotaSize: userQuota.quotaSize, + quotaQueryCount: userQuota.quotaQueryCount, + usageSize: currentTotalSize, + usageCount: userQuota.usageCount, + }) + .pipe(catchError(() => of(null))); + } + async save(content: any) { + const userQuota = await firstValueFrom(this.uq.getCurrentUsage()); + const currentTotalSize = await this.totalStorage(content); + + // Check if the current total size is greater than the user's quota size + if (currentTotalSize >= userQuota.quotaSize) { + this.sb.open( + 'Cannot Save Query because Quota Limit reached. Please contact administrator to increase your quota.', + 'Okay', + { + duration: 60000, + }, + ); + return; + } + const { TextInputDialogComponent } = await import( '../../../../../components/text-input-dialog/text-input-dialog.component' ); @@ -91,6 +141,7 @@ export class QueryResultViewerContainerComponent implements OnChanges { placeholder: 'My query results', }, }); + dialog.afterClosed().subscribe((name) => { if (name) { this.ss.start(); @@ -105,6 +156,8 @@ export class QueryResultViewerContainerComponent implements OnChanges { if (!res) { this.sb.open('Saving failed', 'Okay', { duration: 60000 }); } + + this.updateUserQuota(userQuota, currentTotalSize); this.ss.end(); }); } diff --git a/webgui/webapp/src/app/pages/portal-page/query-page/components/query-tab/query-tab.component.ts b/webgui/webapp/src/app/pages/portal-page/query-page/components/query-tab/query-tab.component.ts index 41a56ac..a4ed13b 100644 --- a/webgui/webapp/src/app/pages/portal-page/query-page/components/query-tab/query-tab.component.ts +++ b/webgui/webapp/src/app/pages/portal-page/query-page/components/query-tab/query-tab.component.ts @@ -24,7 +24,7 @@ import { } from 'src/app/utils/parsers'; import { MatDialog } from '@angular/material/dialog'; import { FilterTypes, ScopeTypes } from 'src/app/utils/interfaces'; -import { catchError, of, Subscription } from 'rxjs'; +import { catchError, filter, firstValueFrom, of, Subscription } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import _ from 'lodash'; import { AsyncPipe } from '@angular/common'; @@ -49,6 +49,8 @@ import { twoRangesValidator, } from 'src/app/utils/validators'; import { customQueries } from './custom-queries'; +import { AuthService } from 'src/app/services/auth.service'; +import { UserQuotaService } from 'src/app/services/userquota.service'; // import { result, query, endpoint } from './test_responses/individuals'; // import { result, query } from './test_responses/biosamples'; @@ -140,6 +142,11 @@ export class QueryTabComponent implements OnInit, AfterViewInit, OnDestroy { page!: number; private subscription: Subscription | null = null; + // user quota + protected userSub: string = ''; + protected quotaQueryCount: number = 0; + protected usageCount: number = 0; + constructor( private fb: FormBuilder, private qs: QueryService, @@ -147,6 +154,7 @@ export class QueryTabComponent implements OnInit, AfterViewInit, OnDestroy { public dg: MatDialog, private sb: MatSnackBar, private ss: SpinnerService, + private uq: UserQuotaService, ) { this.form = this.fb.group({ projects: [[], Validators.required], @@ -301,8 +309,21 @@ export class QueryTabComponent implements OnInit, AfterViewInit, OnDestroy { this.openPanels[index] = false; } - run() { + async run() { this.ss.start(); + + const { quotaQueryCount, usageCount, userSub } = await firstValueFrom( + this.uq.getCurrentUsage(), + ); + + if (usageCount >= quotaQueryCount) { + this.sb.open('Run Query is reach quota limit.', 'Okay', { + duration: 60000, + }); + this.ss.end(); + return; + } + const form: any = this.form.value; const query = { projects: form.projects, @@ -345,6 +366,10 @@ export class QueryTabComponent implements OnInit, AfterViewInit, OnDestroy { this.results = data; this.endpoint = endpoint; this.scope = form.customReturn ? form.return : form.scope; + + this.uq.incrementUsageCount(userSub).subscribe(() => { + console.log('usage count incremented'); + }); } else { this.sb.open( 'API request failed. Please check your parameters.', diff --git a/webgui/webapp/src/app/services/userquota.service.spec.ts b/webgui/webapp/src/app/services/userquota.service.spec.ts new file mode 100644 index 0000000..d399997 --- /dev/null +++ b/webgui/webapp/src/app/services/userquota.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed } from '@angular/core/testing'; +import { UserQuotaService } from './userquota.service'; + +describe('UserQuotaService', () => { + let service: UserQuotaService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserQuotaService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/webgui/webapp/src/app/services/userquota.service.ts b/webgui/webapp/src/app/services/userquota.service.ts new file mode 100644 index 0000000..041be9f --- /dev/null +++ b/webgui/webapp/src/app/services/userquota.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { API } from 'aws-amplify'; +import { catchError, filter, from, map, Observable, of, switchMap } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { AuthService } from './auth.service'; + +@Injectable({ + providedIn: 'root', +}) +export class UserQuotaService { + constructor(private auth: AuthService) {} + + getUserQuota(id: string) { + console.log('get user quota'); + return from( + API.get(environment.api_endpoint_sbeacon.name, `dportal/quota/${id}`, {}), + ); + } + + upsertUserQuota(id: string, costEstimation: number | null, usage: any) { + console.log('upsert user quota'); + return from( + API.post(environment.api_endpoint_sbeacon.name, 'dportal/quota', { + body: { + IdentityUser: id, + CostEstimation: costEstimation, + Usage: usage, + }, + }), + ); + } + + incrementUsageCount(id: string) { + console.log('incrementUsageCount'); + return from( + API.post( + environment.api_endpoint_sbeacon.name, + `dportal/quota/${id}/increment_usagecount`, + {}, + ), + ); + } + + getCurrentUsage(): Observable<{ + userSub: string; + quotaQueryCount: number; + quotaSize: number; + usageCount: number; + usageSize: number; + costEstimation: number; + }> { + return this.auth.user.pipe( + filter((u) => !!u), + switchMap((u: any) => { + const userSub = u.attributes.sub; + return this.getUserQuota(userSub).pipe( + catchError(() => of(null)), + map((res) => ({ + userSub, + quotaQueryCount: res?.Usage.quotaQueryCount || 0, + usageCount: res?.Usage.usageCount || 0, + quotaSize: res?.Usage.quotaSize || 0, + usageSize: res?.Usage.usageSize || 0, + costEstimation: res?.CostEstimation || 0, + })), + ); + }), + ); + } +} diff --git a/webgui/webapp/src/app/utils/file.ts b/webgui/webapp/src/app/utils/file.ts new file mode 100644 index 0000000..efb35c0 --- /dev/null +++ b/webgui/webapp/src/app/utils/file.ts @@ -0,0 +1,68 @@ +const bytesInOneGB = 1024 * 1024 * 1024; // 1024 * 1024 * 1024 Bytes + +/** + * Converts a given number of bytes into a human-readable string format. + * + * @param bytes - The number of bytes to be converted. + * @returns A string representing the size in a more readable format (e.g., '10.24 KB', '1.00 MB'). + * + * @example + * ```typescript + * formatBytes(1024); // '1.00 KB' + * formatBytes(123456789); // '117.74 MB' + * ``` + */ +export function formatBytes(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(0)) + ' ' + sizes[i]; +} + +/** + * Converts a given number of gigabytes into bytes. + * + * @param gigabytes - The number of gigabytes to be converted. + * @returns The equivalent number of bytes. + * + * @example + * ```typescript + * gigabytesToBytes(1); // 1073741824 + * gigabytesToBytes(0.5); // 536870912 + * ``` + */ +export function gigabytesToBytes(gigabytes: number): number { + return gigabytes * bytesInOneGB; +} + +/** + * Converts a given number of bytes into gigabytes. + * + * @param bytes - The number of bytes to be converted. + * @returns The equivalent number of gigabytes. + * + * @example + * ```typescript + * bytesToGigabytes(1073741824); // 1 + * bytesToGigabytes(536870912); // 0.5 + * ``` + */ +export function bytesToGigabytes(bytes: number): number { + return bytes / bytesInOneGB; +} + +/** + * Calculates the total size of all items in a given data storage object. + * + * @param dataStorage - The data storage object containing the items to calculate the total size of. + * @returns The total size of all items in the data storage object. + * + * @example + * ```typescript + * getTotalSize(dataStorage); // 123456789 + * ``` + */ +export function getTotalStorageSize(dataStorage: any): number { + return dataStorage.reduce((total: number, item: any) => total + item.size, 0); +} diff --git a/webgui/webapp/src/assets/images/header.png b/webgui/webapp/src/assets/images/header.png index 18e17f6..f9cae49 100755 Binary files a/webgui/webapp/src/assets/images/header.png and b/webgui/webapp/src/assets/images/header.png differ diff --git a/webgui/webapp/src/styles.scss b/webgui/webapp/src/styles.scss index b257458..3d294bd 100644 --- a/webgui/webapp/src/styles.scss +++ b/webgui/webapp/src/styles.scss @@ -12,17 +12,44 @@ Tailwind @include mat.core(); -$bui-primary: mat.define-palette(mat.$cyan-palette, 900, 800, 700); +$custom-contrast: ( + 50: black, +); + +$custom-color: ( + 10: #e1fbfa, + 50: #16b3ac, + 60: #128f8a, + 100: #b3ece8, + 200: #80e1dc, + 300: #4dd6d0, + 400: #26cbc4, + 500: #16b3ac, + // primary color + 600: #14a59d, + 700: #12878d, + 800: #106a7e, + 900: #0d4e6e, + A100: #a1fdfc, + A200: #6ef4f4, + A400: #3beceb, + A700: #21e4e3, + contrast: $custom-contrast, + + // Add contrast map here +); + +$bui-primary: mat.define-palette($custom-color, 50); $bui-accent: mat.define-palette(mat.$cyan-palette, A400, A200, A700); $bui-theme: mat.define-light-theme( ( color: ( primary: $bui-primary, - accent: $bui-accent + accent: $bui-accent, ), typography: mat.define-typography-config(), - density: 0 + density: 0, ) ); @@ -40,7 +67,7 @@ body { } body { - background: #2f2f2f url(assets/images/bg02.jpg) repeat; + background: theme("colors.primary-10"); font-family: "Source Sans Pro", sans-serif; font-size: 12pt; font-weight: 300; @@ -48,7 +75,9 @@ body { } .bui-card-background { - background-color: #1d4753; - background-image: url(/assets/images/dot-grid.png); + background-color: theme("colors.primary-10"); } +button { + color: white !important; +} diff --git a/webgui/webapp/tailwind.config.js b/webgui/webapp/tailwind.config.js index 894b7ea..7948205 100644 --- a/webgui/webapp/tailwind.config.js +++ b/webgui/webapp/tailwind.config.js @@ -3,14 +3,17 @@ module.exports = { corePlugins: { preflight: false, }, - content: [ - "./src/**/*.{html,ts}", - ], + content: ["./src/**/*.{html,ts}"], theme: { - extend: {}, + extend: { + colors: { + "primary-10": "#E1FBFA", + "primary-50": "#16B3AC", + "primary-60": "#128F8A", + "warning-10": "#FFA705", + // Add your custom color here + }, + }, }, - plugins: [ - - ], -} - + plugins: [], +};