From 308444f1a45444af42e98d358eefc1f6c753ea11 Mon Sep 17 00:00:00 2001 From: "alec.schmidt" <alec.schmidt@etu.hesge.ch> Date: Sun, 11 Jun 2023 00:02:45 +0200 Subject: [PATCH] misc changes and upgrades --- frontend/src/app/Types/types.ts | 1 - frontend/src/app/admin/admin.component.html | 31 +++++++++++++---- frontend/src/app/admin/admin.component.ts | 21 ++++++++++++ frontend/src/app/app.component.html | 1 + frontend/src/app/app.component.ts | 4 +-- frontend/src/app/app.module.ts | 6 +++- .../deletion-confirmation.component.css | 0 .../deletion-confirmation.component.html | 14 ++++++++ .../deletion-confirmation.component.spec.ts | 21 ++++++++++++ .../deletion-confirmation.component.ts | 12 +++++++ .../create-category.component.css | 0 .../create-category.component.html | 19 +++++++++++ .../create-category.component.spec.ts | 21 ++++++++++++ .../create-category.component.ts | 34 +++++++++++++++++++ .../app/services/authentication.service.ts | 6 +++- .../src/app/services/questions.service.ts | 6 ++++ frontend/src/app/services/users.service.ts | 7 ++++ .../user-dropdown.component.html | 6 +++- .../user-dropdown/user-dropdown.component.ts | 5 ++- .../update-user/update-user.component.html | 3 +- .../update-user/update-user.component.ts | 17 +++++++--- 21 files changed, 216 insertions(+), 19 deletions(-) create mode 100644 frontend/src/app/deletion-confirmation/deletion-confirmation.component.css create mode 100644 frontend/src/app/deletion-confirmation/deletion-confirmation.component.html create mode 100644 frontend/src/app/deletion-confirmation/deletion-confirmation.component.spec.ts create mode 100644 frontend/src/app/deletion-confirmation/deletion-confirmation.component.ts create mode 100644 frontend/src/app/question-shit/create-category/create-category.component.css create mode 100644 frontend/src/app/question-shit/create-category/create-category.component.html create mode 100644 frontend/src/app/question-shit/create-category/create-category.component.spec.ts create mode 100644 frontend/src/app/question-shit/create-category/create-category.component.ts diff --git a/frontend/src/app/Types/types.ts b/frontend/src/app/Types/types.ts index 21e0fd0..230a4b3 100644 --- a/frontend/src/app/Types/types.ts +++ b/frontend/src/app/Types/types.ts @@ -24,6 +24,5 @@ export type Answer = { } export type Category = { - id: number; category: string; }; \ No newline at end of file diff --git a/frontend/src/app/admin/admin.component.html b/frontend/src/app/admin/admin.component.html index ac032df..b130972 100644 --- a/frontend/src/app/admin/admin.component.html +++ b/frontend/src/app/admin/admin.component.html @@ -11,12 +11,17 @@ <td class="whitespace-nowrap px-6 py-4">{{user.username}}</td> <td class="whitespace-nowrap px-6 py-4">{{user.admin}}</td> <td><button class="bg-teal-200 w-fit border-2 border-teal-600 rounded-lg px-2 hover:bg-teal-400 mt-2" (click)="showUpdateModal(user)">UPDATE</button></td> - <td><button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-500 mt-2" (click)="deleteUser(user)">DELETE</button></td> + <td><button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-500 mt-2" (click)="deleteUserConfirm(user)">DELETE</button></td> </tr> </tbody> </table> - <button (click)="modalQuestionCreate=true;" class="mt-20 bg-white w-fit border-2 border-black rounded-lg px-2 hover:bg-gray-300 mt-2">Create Question</button> + <div class="grid grid-cols-2 gap-10"> + <div class="flex flex-row justify-between"> + <button (click)="modalQuestionCreate=true;" class="mt-20 bg-white w-fit border-2 border-black rounded-lg px-2 hover:bg-gray-300 mt-2">Create Question</button> + <button (click)="modalCategoryCreate=true;" class="mt-20 bg-white w-fit border-2 border-black rounded-lg px-2 hover:bg-gray-300 mt-2">Create Category</button> + </div> + </div> <div class="grid grid-cols-2 gap-10"> <table class="table-auto w-full h-fit text-left"> @@ -26,14 +31,15 @@ </thead> <tbody> <tr *ngFor="let question of questions" class="border-b dark:border-neutral-500"> - <td class="whitespace-nowrap px-6 py-4 max-w-sm break-all">{{question.question}}</td> + <td class="px-6 py-4 max-w-sm break-all">{{question.question}}</td> <td class="whitespace-nowrap px-6 py-4">{{question.category}}</td> <td><button class="bg-teal-200 w-fit border-2 border-teal-600 rounded-lg px-2 hover:bg-teal-400 mt-2" (click)="showQuestionUpdateModal(question)">UPDATE</button></td> - <td><button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-300 mt-2" (click)="deleteQuestion(question)">DELETE</button></td> + <td><button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-300 mt-2" (click)="deleteQuestionConfirm(question)">DELETE</button></td> <td><button class="bg-teal-200 w-fit border-2 border-teal-600 rounded-lg px-2 hover:bg-teal-400 mt-2" (click)="showSelectedAnswers(question.id)">⇉</button></td> </tr> </tbody> </table> + <table class="table-auto w-full h-fit text-left"> <thead class="border-b font-medium dark:border-neutral-500"> <th class="px-6 py-4">Answer</th> @@ -44,7 +50,7 @@ <td class="whitespace-nowrap px-6 py-4">{{answer.text_answer}}</td> <td class="whitespace-nowrap px-6 py-4">{{answer.correct}}</td> <td><button class="bg-teal-200 w-fit border-2 border-teal-600 rounded-lg px-2 hover:bg-teal-400 mt-2" (click)="showAnswerUpdateModal(answer)">UPDATE</button></td> - <td><button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-300 mt-2" (click)="deleteAnswer(answer)">DELETE</button></td> + <td><button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-300 mt-2" (click)="deleteAnswerConfirm(answer)">DELETE</button></td> </tr> </tbody> <button *ngIf="buttonCreateAnswer" class=" bg-white w-fit border-2 border-black rounded-lg px-2 hover:bg-gray-300 mt-2" (click)="modalAnswerCreate = true;">Add Answer</button> @@ -52,7 +58,17 @@ </div> - + <div *ngIf="confirmUserDelete"> + <app-deletion-confirmation (confirm)="deleteUser(userToEdit)" (cancel)="confirmUserDelete=false" ></app-deletion-confirmation> + </div> + + <div *ngIf="confirmQuestionDelete"> + <app-deletion-confirmation (confirm)="deleteQuestion(questionToEdit)" (cancel)="confirmQuestionDelete=false" ></app-deletion-confirmation> + </div> + + <div *ngIf="confirmAnswerDelete"> + <app-deletion-confirmation (confirm)="deleteAnswer(answerToEdit)" (cancel)="confirmAnswerDelete=false" ></app-deletion-confirmation> + </div> <div *ngIf="modalUpdate"> <app-update-user [user]="userToEdit" (closeModal)="modalUpdate=false;"></app-update-user> @@ -76,4 +92,7 @@ <div *ngIf="modalAnswerUpdate"> <app-update-answer [answer]="answerToEdit" (closeModal)="modalAnswerUpdate=false" ></app-update-answer> </div> + <div *ngIf="modalCategoryCreate"> + <app-create-category (closeModal)="modalCategoryCreate=false"></app-create-category> + </div> </div> diff --git a/frontend/src/app/admin/admin.component.ts b/frontend/src/app/admin/admin.component.ts index 60bcd6a..70e60bd 100644 --- a/frontend/src/app/admin/admin.component.ts +++ b/frontend/src/app/admin/admin.component.ts @@ -30,6 +30,10 @@ export class AdminComponent implements OnInit { modalQuestionUpdate: boolean = false; modalAnswerCreate: boolean = false; modalAnswerUpdate: boolean = false; + modalCategoryCreate: boolean = false; + confirmUserDelete: boolean = false; + confirmQuestionDelete: boolean = false; + confirmAnswerDelete: boolean = false; constructor( private router: Router, @@ -70,14 +74,17 @@ export class AdminComponent implements OnInit { deleteUser(user:User) { this.userController.deleteUser(user); + this.confirmUserDelete = false; } deleteQuestion(question: Question) { this.questionController.deleteQuestion(question); + this.confirmQuestionDelete = false; } deleteAnswer(answer: Answer) { this.questionController.deleteAnswer(answer); + this.confirmAnswerDelete = false; } showUpdateModal(user: User){ @@ -95,4 +102,18 @@ export class AdminComponent implements OnInit { this.modalAnswerUpdate = true; } + deleteUserConfirm(user: User) { + this.userToEdit = user; + this.confirmUserDelete = true; + } + + deleteQuestionConfirm(question: Question) { + this.questionToEdit = question; + this.confirmQuestionDelete = true; + } + + deleteAnswerConfirm(answer: Answer) { + this.answerToEdit = answer; + this.confirmAnswerDelete = true; + } } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index f70ebd7..e738f97 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -20,6 +20,7 @@ <app-user-dropdown [username]="auth.getUsername()"></app-user-dropdown> </div> + <main class="w-full h-full"> <router-outlet> </router-outlet> diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 76b9dad..00645a0 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -10,7 +10,7 @@ import { AuthenticationService } from './services/authentication.service'; export class AppComponent { title = 'frontend'; - userDropdownModal: boolean = false + userDropdownModal: boolean = false; constructor( public router: Router, @@ -26,4 +26,4 @@ export class AppComponent { this.userDropdownModal = !this.userDropdownModal; } -} +} \ No newline at end of file diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 338f77e..28e0c12 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -16,6 +16,8 @@ import { CreateAnswerComponent } from './question-shit/create-answer/create-answ import { UpdateQuestionComponent } from './question-shit/update-question/update-question.component'; import { UpdateAnswerComponent } from './question-shit/update-answer/update-answer.component'; import { UserDropdownComponent } from './user-dropdown/user-dropdown.component'; +import { CreateCategoryComponent } from './question-shit/create-category/create-category.component'; +import { DeletionConfirmationComponent } from './deletion-confirmation/deletion-confirmation.component'; @NgModule({ declarations: [ @@ -29,7 +31,9 @@ import { UserDropdownComponent } from './user-dropdown/user-dropdown.component'; CreateAnswerComponent, UpdateQuestionComponent, UpdateAnswerComponent, - UserDropdownComponent + UserDropdownComponent, + CreateCategoryComponent, + DeletionConfirmationComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/deletion-confirmation/deletion-confirmation.component.css b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/deletion-confirmation/deletion-confirmation.component.html b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.html new file mode 100644 index 0000000..413da4a --- /dev/null +++ b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.html @@ -0,0 +1,14 @@ +<div class="z-10 relative"> + <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div> + <div class="fixed inset-0 z-10 overflow-y-auto"> + <div class="flex flex-col min-h-full justify-center p-4 text-center items-center sm:p-0"> + <div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 p-2 w-48"> + <h1 class="mt-2 mx-2 font-bold">Are you sure ?</h1> + <div class="flex flex-row justify-between"> + <button class="bg-red-400 w-fit border-2 border-red-600 rounded-lg px-2 hover:bg-red-500 mt-2" (click)="confirm.emit()">Confirm</button> + <button class="bg-teal-200 w-fit border-2 border-teal-600 rounded-lg px-2 hover:bg-teal-400 mt-2" (click)="cancel.emit()">Cancel</button> + </div> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/frontend/src/app/deletion-confirmation/deletion-confirmation.component.spec.ts b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.spec.ts new file mode 100644 index 0000000..0157e75 --- /dev/null +++ b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeletionConfirmationComponent } from './deletion-confirmation.component'; + +describe('DeletionConfirmationComponent', () => { + let component: DeletionConfirmationComponent; + let fixture: ComponentFixture<DeletionConfirmationComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DeletionConfirmationComponent] + }); + fixture = TestBed.createComponent(DeletionConfirmationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/deletion-confirmation/deletion-confirmation.component.ts b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.ts new file mode 100644 index 0000000..720e5c9 --- /dev/null +++ b/frontend/src/app/deletion-confirmation/deletion-confirmation.component.ts @@ -0,0 +1,12 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'app-deletion-confirmation', + templateUrl: './deletion-confirmation.component.html', + styleUrls: ['./deletion-confirmation.component.css'] +}) +export class DeletionConfirmationComponent { + + @Output() cancel: EventEmitter<void> = new EventEmitter<void>(); + @Output() confirm: EventEmitter<void> = new EventEmitter<void>(); +} diff --git a/frontend/src/app/question-shit/create-category/create-category.component.css b/frontend/src/app/question-shit/create-category/create-category.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/question-shit/create-category/create-category.component.html b/frontend/src/app/question-shit/create-category/create-category.component.html new file mode 100644 index 0000000..386b36d --- /dev/null +++ b/frontend/src/app/question-shit/create-category/create-category.component.html @@ -0,0 +1,19 @@ +<div class="z-10 relative"> + <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div> + <div class="fixed inset-0 z-10 overflow-y-auto"> + <div class="flex flex-col min-h-full justify-center p-4 text-center items-center sm:p-0"> + <div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 "> + <h1 class="mt-2 mx-2 font-bold">Create Category</h1> + + <form [formGroup]="createCategory" (ngSubmit)="onSubmit()" class="flex flex-col pb-2 mx-2"> + <input class="border-b-4 mt-2 w-full" type="text" placeholder="Text" formControlName="category"> + <div class="flex flex-row justify-between"> + <button class="border-2 border-black rounded-lg px-2 hover:bg-gray-300 mt-2" type="submit">Create</button> + <button class="border-2 border-black rounded-lg px-2 hover:bg-gray-300 mt-2" (click)="closeModal.emit()">Close</button> + </div> + </form> + + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/frontend/src/app/question-shit/create-category/create-category.component.spec.ts b/frontend/src/app/question-shit/create-category/create-category.component.spec.ts new file mode 100644 index 0000000..ad29077 --- /dev/null +++ b/frontend/src/app/question-shit/create-category/create-category.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateCategoryComponent } from './create-category.component'; + +describe('CreateCategoryComponent', () => { + let component: CreateCategoryComponent; + let fixture: ComponentFixture<CreateCategoryComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [CreateCategoryComponent] + }); + fixture = TestBed.createComponent(CreateCategoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/question-shit/create-category/create-category.component.ts b/frontend/src/app/question-shit/create-category/create-category.component.ts new file mode 100644 index 0000000..55a47e5 --- /dev/null +++ b/frontend/src/app/question-shit/create-category/create-category.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Category } from 'src/app/Types/types'; +import { QuestionsService } from 'src/app/services/questions.service'; + +@Component({ + selector: 'app-create-category', + templateUrl: './create-category.component.html', + styleUrls: ['./create-category.component.css'] +}) +export class CreateCategoryComponent { + createCategory: FormGroup; + + @Output() closeModal: EventEmitter<string> = new EventEmitter<string>(); + + constructor(private questionsController: QuestionsService) { } + + ngOnInit(): void { + this.createCategory = new FormGroup({ + category: new FormControl('', Validators.required) + }); + } + + onSubmit() { + const category = this.createCategory.get("category")!.value; + + const newCategory: Category = { + category: category + } + + this.questionsController.addCategory(newCategory); + this.closeModal.emit(); + } +} diff --git a/frontend/src/app/services/authentication.service.ts b/frontend/src/app/services/authentication.service.ts index dbe901d..da827c6 100644 --- a/frontend/src/app/services/authentication.service.ts +++ b/frontend/src/app/services/authentication.service.ts @@ -59,10 +59,14 @@ export class AuthenticationService { } getUsername(): string { - const user = this.getDecodedAccessToken() as User; + const user = this.getUser(); if(user === null) return "Guest"; return user.username; } + + getUser(): User { + return this.getDecodedAccessToken() as User; + } } diff --git a/frontend/src/app/services/questions.service.ts b/frontend/src/app/services/questions.service.ts index 1e15176..a9f7d27 100644 --- a/frontend/src/app/services/questions.service.ts +++ b/frontend/src/app/services/questions.service.ts @@ -84,4 +84,10 @@ export class QuestionsService { }) } + addCategory(category: Category) { + this.http.post(ROUTE + "/category", category).subscribe(() => { + this.fetchCategories(); + }) } + +} diff --git a/frontend/src/app/services/users.service.ts b/frontend/src/app/services/users.service.ts index 7ceb9a3..2217183 100644 --- a/frontend/src/app/services/users.service.ts +++ b/frontend/src/app/services/users.service.ts @@ -51,4 +51,11 @@ export class UsersService { this.http.patch<User>(ROUTE + "/" + user.id.toString(), user).subscribe(res => this.fetchUsers()) } + updateUsername(user: User) { + this.http.patch<User>(ROUTE + "/" + user.id.toString(), user).subscribe(res => { + this.auth.logout(); + this.auth.login(user.username, user.password); + }) + } + } diff --git a/frontend/src/app/user-dropdown/user-dropdown.component.html b/frontend/src/app/user-dropdown/user-dropdown.component.html index acdc5d2..daf5894 100644 --- a/frontend/src/app/user-dropdown/user-dropdown.component.html +++ b/frontend/src/app/user-dropdown/user-dropdown.component.html @@ -4,8 +4,12 @@ <div class="rounded-lg bg-gray-300 p-2"> <h1 class="font-bold">User Infos</h1> <h1>{{username}}</h1> - <button *ngIf="auth.isLoggedIn()" class="border-2 border-black rounded-lg px-2 bg-gray-400 hover:bg-gray-500">Change Username</button> + <button *ngIf="auth.isLoggedIn()" class="border-2 border-black rounded-lg px-2 bg-gray-400 hover:bg-gray-500" (click)="userUpdateModal=true">Change Username</button> </div> </div> +</div> + +<div *ngIf="userUpdateModal"> + <app-update-user [user]="auth.getUser()" (closeModal)="userUpdateModal=false" ></app-update-user> </div> \ No newline at end of file diff --git a/frontend/src/app/user-dropdown/user-dropdown.component.ts b/frontend/src/app/user-dropdown/user-dropdown.component.ts index 5eb79ca..1071ad3 100644 --- a/frontend/src/app/user-dropdown/user-dropdown.component.ts +++ b/frontend/src/app/user-dropdown/user-dropdown.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { User } from '../Types/types'; import { AuthenticationService } from '../services/authentication.service'; +import { UsersService } from '../services/users.service'; @Component({ selector: 'app-user-dropdown', @@ -10,8 +11,10 @@ import { AuthenticationService } from '../services/authentication.service'; export class UserDropdownComponent implements OnInit { @Input() username!: string; + userUpdateModal: boolean = false; - constructor(public auth: AuthenticationService) {} + constructor(public auth: AuthenticationService, + private userController: UsersService) {} ngOnInit(): void { if (this.username === undefined) diff --git a/frontend/src/app/user-shit/update-user/update-user.component.html b/frontend/src/app/user-shit/update-user/update-user.component.html index e302801..b498383 100644 --- a/frontend/src/app/user-shit/update-user/update-user.component.html +++ b/frontend/src/app/user-shit/update-user/update-user.component.html @@ -7,7 +7,8 @@ <form [formGroup]="updateUser" (ngSubmit)="onSubmit()" class="flex flex-col pb-2 mx-2"> <input class="border-b-4 mt-2 w-full" type="text" formControlName="username"> - <div class="my-2 border-b-4"> + <input *ngIf="!auth.isAdmin()" class="border-b-4 mt-2 w-full" type="password" placeholder="Password" formControlName="password"> + <div *ngIf="auth.isAdmin()" class="my-2 border-b-4"> <label class="mt-2 w-fit">Admin</label><input class="ml-6" type="checkbox" formControlName="admin"> </div> <div class="flex flex-row justify-between"> diff --git a/frontend/src/app/user-shit/update-user/update-user.component.ts b/frontend/src/app/user-shit/update-user/update-user.component.ts index 749e53d..a997185 100644 --- a/frontend/src/app/user-shit/update-user/update-user.component.ts +++ b/frontend/src/app/user-shit/update-user/update-user.component.ts @@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { User } from '../../Types/types'; import { UsersService } from '../../services/users.service'; +import { AuthenticationService } from 'src/app/services/authentication.service'; @Component({ selector: 'app-update-user', @@ -10,33 +11,39 @@ import { UsersService } from '../../services/users.service'; }) export class UpdateUserComponent implements OnInit { - updateUser: FormGroup + updateUser: FormGroup; @Input() user!: User; @Output() closeModal: EventEmitter<void> = new EventEmitter<void>(); - constructor(private userController: UsersService) { } + constructor(private userController: UsersService, + public auth: AuthenticationService) { } ngOnInit(): void { this.updateUser = new FormGroup({ username: new FormControl(this.user.username, Validators.required), + password: new FormControl('', Validators.required), admin: new FormControl(this.user.admin, Validators.required) }); } onSubmit() { const username = this.updateUser.get("username")!.value; - + const password = this.updateUser.get("password")!.value; const type = this.updateUser.get("admin")!.value; const userToEdit: User = { id: this.user.id, username : username, - password: undefined, + password: password, admin: type } - this.userController.updateUser(userToEdit); + if (this.auth.isAdmin()) + this.userController.updateUser(userToEdit); + else + this.userController.updateUsername(userToEdit); + this.closeModal.emit(); } } -- GitLab