Code analysis (#38)

Docs, no review needed
This commit is contained in:
Mohammed Fareed 2023-12-04 01:04:10 -05:00 committed by GitHub
parent 6efab1fe9e
commit b88fece59b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 207 additions and 232 deletions

View file

@ -22,13 +22,13 @@ The project consists of a web application that allows managers (school administr
### Glossary and Acronyms
> _**[Sprint 2 & 4]** Provide a table of terms and acronyms._
| Term | Definition |
|------|------------|
| SPA | Single Page |
| MVP | Minimum Viable Product |
| DAO | Data Access Object |
| API | Application Programming Interface |
| UI | User Interface |
| Term | Definition |
| ---- | --------------------------------- |
| SPA | Single Page |
| MVP | Minimum Viable Product |
| DAO | Data Access Object |
| API | Application Programming Interface |
| UI | User Interface |
## Requirements
@ -363,7 +363,6 @@ classDiagram
```
## OO Design Principles
> _**[Sprint 2, 3 & 4]** Will eventually address up to **4 key OO Principles** in your final design. Follow guidance in augmenting those completed in previous Sprints as indicated to you by instructor. Be sure to include any diagrams (or clearly refer to ones elsewhere in your Tier sections above) to support your claims._
### Single Responsibility Principle
@ -371,45 +370,80 @@ The single responsibility principle has been used by ensuring models, DAOs, cont
### Controller Design Pattern
The controller design pattern is used by the application to separate the logic of the application from the view. A backend deploys this design pattern by having controller classes that handles requests from the view and provides the view with the data it needs. The controller class is also responsible for handling the logic of the application, such as creating needs and funding baskets. This allows the view to focus on displaying the data and allows the controller to focus on handling the business logic of the application.
The controller design pattern is used by the application to separate the logic of the application from the view. A backend deploys this design pattern by having controller classes that handles requests from the view and provides the view with the data it needs. The controller class has this single responsibility; the business logic is delegated to models. This allows the view to focus on displaying the data and allows the controller to focus on controlling the services of the application, which handle the business logic of the application. The ViewModel Tier class diagrams shows that the controller classes access DAO classes to handle the logic of the application.
This design pattern is applied to the `CupboardController` and `BasketController` classes. The controller design pattern is used to separate the logic of the application from the view. The controller classes are responsible for handling the requests from the view and providing the view with the data it needs. The controller classes are also responsible for handling the logic of the application, such as creating needs and funding baskets. This allows the view to focus on displaying the data and allows the controller to focus on handling the business logic of the application.
> _**[Sprint 3 & 4]** OO Design Principles should span across **all tiers.**_
### Dependency Injection Design Pattern
Dependency injection is throughout the project through Angular's dependency injection system and Maven's dependency management system. The dependency injection system is used to inject services into components. This allows the components to focus on their single responsibility of displaying data and allows the services to focus on their single responsibility of providing the data. This also allows the components to be easily tested by injecting mock services into them. This is done extensively when testing controllers.
The design pattern is enforced by ensuring that Angular components only depend on services and not other components, delegating responsibility of retrieving data and working with the backend to services. Services have been created for each backend controller, and are injected into the components that need them. Each service is responsible for providing access points to the endpoints of its respective backend controller. For example, the `BasketService` is responsible for providing access points to the `FundingBasketController` endpoints, which is injected into the components that need access to funding baskets, such as the list components of the `BasketModule`.
### Open-Closed Principle
The open-closed principle is used by ensuring that the application is open for extension but closed for modification. This is done by ensuring that the application is modular and that each module is responsible for a single functionality. This allows the application to be extended by adding new modules without modifying existing modules. This is done by ensuring that the application is built using the Model-View-ViewModel architecture pattern. This allows the application to be extended by adding new models, views, and view models without modifying existing models, views, and view models.
This principle is applied to various components of the model tier; the class diagrams show vertically independent paths, showing independence of models from each other. This allows the application to be extended by adding new models without modifying existing models. The class diagrams also show that the models are horizontally dependent on the DAOs, showing that the models are closed for modification but open for extension. This allows the application to be extended by adding new DAOs without modifying existing models.
## Static Code Analysis/Future Design Improvements
> _**[Sprint 4]** With the results from the Static Code Analysis exercise,
> **Identify 3-4** areas within your code that have been flagged by the Static Code
> Analysis Tool (SonarQube) and provide your analysis and recommendations.
> Include any relevant screenshot(s) with each area._
### Static Code Analysis
Static code analysis was performed using SonarQube on the Java backend. The following is the results of the analysis.
![SonarQube API Analysis](code_analysis/api_measures.png)
The results show that the API has a maintainability, reliability, and security rating of A. The results also show that the API has a coverage of 86.1%, which do not match the results obtained using `mvn clean test`; the latter was used as the source of truth for the coverage of the API. The report shows that the API has 0 bugs, 0 vulnerabilities, but 314 code smells. The report also shows no security hot-spots, with 2.5% of the code being duplicated.
The code smells were further analyzed to determine their severity. The following is the results of the analysis.
![SonarQube API Issues](code_analysis/api_issues.png)
The results show that the API has 15, 48, 251 code smells of high, medium, and low severity respectively. The results also show that all the issues affect maintainability, with most of them due to intentionality.
The high severity issues were further analyzed to determine their impact on the maintainability of the API. The following is the results of the analysis.
![SonarQube API Critical Issues](code_analysis/api_issues_high.png)
The results show that the API has 15 critical issues, 9 of which are due to intentionality and 6 due to adaptability. The results also show that many of the issues relate to multi-threading and design, which are the result of unfamiliarity with the frameworks.
The Angular frontend was also analyzed using SonarQube. The following is the results of the analysis.
![SonarQube UI Analysis](code_analysis/ui_measures.png)
The results show that the UI has a maintainability, reliability, and security rating of A. The results also show that the UI has 0 security hot-spots, bugs, vulnerabilities, code smells, and 0% duplicate code. The results do show a failed overall score due to the lack of coverage, which is expected since the UI is not unit tested.
The code smells were further analyzed to determine their severity. The following is the results of the analysis.
![SonarQube UI Issues](code_analysis/ui_issues.png)
The results show that the UI has 3, 19, 19 code smells of high, medium, and low severity respectively. The results also show that all the issues affect maintainability, with all of them due to intentionality except 4, which are due to consistency (attributed to more individual work during phase 2).
The high severity issues were further analyzed to determine their impact on the maintainability of the UI. The following is the results of the analysis.
![SonarQube UI Critical Issues](code_analysis/ui_issues_high.png)
The results show that the UI has 3 critical issues, all of which are due to intentionality. The results also show that all the issues are due to bad-practices, which are the result of unfamiliarity with the frameworks.
### Future Design Improvements
> _**[Sprint 4]** Discuss **future** refactoring and other design improvements your team would explore if the team had additional time._
## Testing
### Acceptance Testing
> _**[Sprint 2 & 4]** Report on the number of user stories that have passed all their
> acceptance criteria tests, the number that have some acceptance
> criteria tests failing, and the number of user stories that
> have not had any testing yet. Highlight the issues found during
> acceptance testing and if there are any concerns._
All the user stories have passed their acceptance criteria tests. In total, 23 user stories have been implemented, 14 of which are user facing and included in the testing plan. The acceptance plan shows 40 acceptance criteria tests, all of which have passed. The MVP features and one of the 10% features have been implemented and are bug free.
All the user stories have passed their acceptance criteria tests. In total, 23 user stories have been implemented, 14 of which are user facing and included in the testing plan. The acceptance plan shows 40 acceptance criteria tests, all of which have passed. The MVP features and one of the 10% features have been implemented and are bug free. The second 10% feature has been implemented but has not been tested yet. One issue found during acceptance testing that was not resolved is the view not consistently updating upon changes to the model. The issue stemmed from the bug not consistently being present in different environment. This is due to the lack of familiarity with Angular and the frameworks used. The issue was not resolved due to the lack of time.
### Unit Testing and Code Coverage
> _**[Sprint 4]** Discuss your unit testing strategy. Report on the code coverage
> achieved from unit testing of the code base. Discuss the team's
> coverage targets, why you selected those values, and how well your
> code coverage met your targets._
Unit testing was performed and the results analyzed. The following is the results of the analysis.
![API Code Coverage](coverage/api.png)
The final API shows an average coverage of 92%, which is above the recommended 90%.
#### Controller Tier
This is the analysis at the Controller Tier code for the project.

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View file

@ -243,15 +243,23 @@ public class Need {
* {@inheritDoc}
*/
@Override
public boolean equals(Object otherObject){
if(!(otherObject instanceof Need))
public boolean equals(Object otherObject) {
if (!(otherObject instanceof Need))
return false;
Need castObject = (Need)otherObject;
Need castObject = (Need) otherObject;
return this.id == castObject.id &&
this.name.equals(castObject.name) &&
this.amount_unit.equals(castObject.amount_unit) &&
this.amount_needed == castObject.amount_needed &&
this.amount_in_stock == castObject.amount_in_stock &&
this.tags.equals(castObject.tags);
this.name.equals(castObject.name) &&
this.amount_unit.equals(castObject.amount_unit) &&
this.amount_needed == castObject.amount_needed &&
this.amount_in_stock == castObject.amount_in_stock &&
this.tags.equals(castObject.tags);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.id;
}
}

View file

@ -76,12 +76,20 @@ public class Notification {
* {@inheritDoc}
*/
@Override
public boolean equals(Object anotherObject){
if(!(anotherObject instanceof Notification))
public boolean equals(Object anotherObject) {
if (!(anotherObject instanceof Notification))
return false;
Notification castObject = (Notification) anotherObject;
return this.id == castObject.id &&
this.needID == castObject.id &&
this.message == castObject.message;
return this.id == castObject.id &&
this.needID == castObject.id &&
this.message == castObject.message;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.id;
}
}

View file

@ -10,16 +10,16 @@ import java.util.logging.Logger;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.SecretKeyFactory;
public class User{
public class User {
private static final Logger LOG = Logger.getLogger(User.class.getName());
private static final String STRING_FORMAT = "User [id=%d, first=%s, last=%s, username='%s', hash='%s', admin='%s', salt='%s']";
//Using recommended algorithm by OWASP, which is built directly into Java
// Using recommended algorithm by OWASP, which is built directly into Java
private final String KEY_GEN_ALGORITHM = "PBKDF2WithHmacSHA256";
private final int KEY_LENGTH = 128;
//OWASP recommended value >=600,000
//Value used is 2^20
//Larger values take longer to calculate, but are more secure
// OWASP recommended value >=600,000
// Value used is 2^20
// Larger values take longer to calculate, but are more secure
private final int PBKDF_ITERARTIONS = 1048576;
private final SecureRandom random = new SecureRandom();
@ -38,31 +38,31 @@ public class User{
@JsonProperty("admin")
private boolean isAdmin;
public User( @JsonProperty("id") int id,
@JsonProperty("first_name") String first_name,
@JsonProperty("last_name") String last_name,
@JsonProperty("username") String username,
@JsonProperty("password") String clear_pass,
@JsonProperty("admin") boolean is_admin){
public User(@JsonProperty("id") int id,
@JsonProperty("first_name") String first_name,
@JsonProperty("last_name") String last_name,
@JsonProperty("username") String username,
@JsonProperty("password") String clear_pass,
@JsonProperty("admin") boolean is_admin) {
this.id = id;
this.firstName = first_name;
this.lastName = last_name;
this.username = username;
this.isAdmin = is_admin;
if(clear_pass == null)
if (clear_pass == null)
clear_pass = "admin";
this.salt = new byte[256];
random.nextBytes(this.salt);
try{
try {
KeySpec spec = new PBEKeySpec(clear_pass.toCharArray(), salt, PBKDF_ITERARTIONS, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_GEN_ALGORITHM);
this.passHash = factory.generateSecret(spec).getEncoded();
}catch(Exception e){
} catch (Exception e) {
this.passHash = new byte[25];
}
}
public User(int id, User user){
public User(int id, User user) {
this.salt = new byte[256];
this.salt = user.salt;
this.id = id;
@ -73,55 +73,87 @@ public class User{
this.isAdmin = user.isAdmin;
}
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getFirstName() { return this.firstName; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) { this.lastName = lastName; }
public String getLastName() { return this.lastName; }
public String getFirstName() {
return this.firstName;
}
public void setUsername(String username) { this.username = username; }
public String getUsername() { return this.username; }
public void setLastName(String lastName) {
this.lastName = lastName;
}
public boolean isAdmin() { return this.isAdmin; }
public void setAdmin(boolean admin){ this.isAdmin = admin; }
public String getLastName() {
return this.lastName;
}
public int getID() { return this.id; }
public void setUsername(String username) {
this.username = username;
}
public boolean isPassword(String pass_test){
try{
public String getUsername() {
return this.username;
}
public boolean isAdmin() {
return this.isAdmin;
}
public void setAdmin(boolean admin) {
this.isAdmin = admin;
}
public int getID() {
return this.id;
}
public boolean isPassword(String pass_test) {
try {
KeySpec spec = new PBEKeySpec(pass_test.toCharArray(), salt, PBKDF_ITERARTIONS, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_GEN_ALGORITHM);
byte[] newHash = factory.generateSecret(spec).getEncoded();
LOG.info(new String(this.passHash, StandardCharsets.UTF_8));
LOG.info(new String(newHash, StandardCharsets.UTF_8));
int diff = this.passHash.length ^ newHash.length;
for(int i = 0; i < this.passHash.length && i < newHash.length; i++)
{
for (int i = 0; i < this.passHash.length && i < newHash.length; i++) {
diff |= this.passHash[i] ^ newHash[i];
}
return diff == 0;
} catch (Exception e) {
return false;
}
catch (Exception e) { return false; }
}
public String toString(){
return String.format(STRING_FORMAT, this.id, this.firstName, this.lastName, this.username, new String(this.passHash, StandardCharsets.UTF_8), this.isAdmin, new String(this.salt, StandardCharsets.UTF_8));
public String toString() {
return String.format(STRING_FORMAT, this.id, this.firstName, this.lastName, this.username,
new String(this.passHash, StandardCharsets.UTF_8), this.isAdmin,
new String(this.salt, StandardCharsets.UTF_8));
}
/**
*{@inheritDoc}
* {@inheritDoc}
*/
@Override
public boolean equals(Object otherObject){
if(!(otherObject instanceof User))
public boolean equals(Object otherObject) {
if (!(otherObject instanceof User))
return false;
User castObject = (User)otherObject;
User castObject = (User) otherObject;
return this.id == castObject.id &&
this.firstName.equals(castObject.firstName) &&
this.lastName.equals(castObject.lastName) &&
this.username.equals(castObject.username) &&
this.passHash.equals(castObject.passHash) &&
this.salt.equals(castObject.salt) &&
this.isAdmin == castObject.isAdmin;
this.firstName.equals(castObject.firstName) &&
this.lastName.equals(castObject.lastName) &&
this.username.equals(castObject.username) &&
this.passHash == castObject.passHash &&
this.salt == castObject.salt &&
this.isAdmin == castObject.isAdmin;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.id;
}
}

View file

@ -282,7 +282,10 @@ public class CupboardFileDAO implements CupboardDAO {
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
file.createNewFile();
if (!file.createNewFile()) {
LOG.severe("Could not create file " + filename);
throw new IOException("Could not create file " + filename);
}
// write empty JSON array to file
objectMapper.writeValue(file, new Need[0]);
LOG.info(filename + " created");

View file

@ -176,7 +176,10 @@ public class FundingBasketFileDAO implements FundingBasketDAO {
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
file.createNewFile();
if (!file.createNewFile()) {
LOG.severe("Could not create file " + filename);
throw new IOException("Could not create file " + filename);
}
// write empty JSON array to file
objectMapper.writeValue(file, new Need[0]);
LOG.info(filename + " created");

View file

@ -156,7 +156,10 @@ public class NotificationsFileDAO implements NotificationsDAO {
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
file.createNewFile();
if (!file.createNewFile()) {
LOG.severe("Could not create file " + filename);
throw new IOException("Could not create file " + filename);
}
// write empty JSON array to file
objectMapper.writeValue(file, new NotificationsCenter());
LOG.info(filename + " created");

View file

@ -165,7 +165,10 @@ public class UsersFileDAO implements UsersDAO {
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
file.createNewFile();
if (!file.createNewFile()) {
LOG.severe("Could not create file " + filename);
throw new IOException("Could not create file " + filename);
}
// write empty JSON array to file
objectMapper.writeValue(file, new User[0]);
LOG.info(filename + " created");

View file

@ -8,19 +8,32 @@ ARG BACKEND_API_PORT
#Explicitly set working directory in new docker container
WORKDIR /app
#Copy the frontend folder into the working directory
COPY . /app/
# COPY . /app/
COPY ./src /app/src
COPY ./.editorconfig /app/.editorconfig
COPY ./angular.json /app/angular.json
COPY ./default.conf /app/default.conf
COPY ./package-lock.json /app/package-lock.json
COPY ./package.json /app/package.json
COPY ./replace_envriotment_variables.js /app/replace_envriotment_variables.js
COPY ./tsconfig.app.json /app/tsconfig.app.json
COPY ./tsconfig.json /app/tsconfig.json
COPY ./tsconfig.spec.json /app/tsconfig.spec.json
#Modify environment file with set values
RUN sed -i "s/localhost/$BACKEND_API_URL/g" /app/src/proxy.conf.mjs
RUN sed -i "s/8080/$BACKEND_API_PORT/g" /app/src/proxy.conf.mjs
#Install all required NodeJS packages
RUN npm install
RUN npm install --ignore-scripts
#Build NodeJS package (executes ng build)
RUN npm run build #-- --configuration=docker
#Use Nginx as web server
FROM nginx:1.25.3 as frontend
#Create and set non-root user
RUN addgroup -S app && adduser -S app -G app
USER app
#Copy the static files built by NodeJS into the Nginx server's proper location
COPY --from=frontend-build /app/dist/ufund-ui /usr/share/nginx/html
#Proxy out relevant parts of the URL

View file

@ -7,7 +7,7 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';
import { fakeBackendProvider, JwtInterceptor, ErrorInterceptor } from './helpers';
import { JwtInterceptor, ErrorInterceptor } from './helpers';
import { AlertComponent } from './components/alert/alert.component';
@ -25,13 +25,12 @@ import { AlertComponent } from './components/alert/alert.component';
RegisterComponent,
AlertComponent,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
fakeBackendProvider,
],
bootstrap: [AppComponent],
})
export class AppModule { }

View file

@ -1,7 +1,7 @@
<div>
<div class="container">
<h1>Funding Basket</h1>
<table class="table table-striped">
<table class="table table-striped" aria-label="Needs List">
<thead>
<tr>
<th>Name</th>

View file

@ -1,132 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, materialize, dematerialize } from 'rxjs/operators';
import { User } from 'src/app/models';
const usersKey = 'angular-tutorial';
let users = [
{ id: "0", firstName: 'Admin', lastName: " ", username: "admin", password: "admin", admin: true},
{ id: "1", firstName: 'Blizzard', lastName: 'Finnegan', username: 'test', password: 'test', admin: true},
{ id: "2", firstName: 'Jason', lastName: 'Watmore', username: 'asdf', password: 'test'},
];
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
return handleRoute();
function handleRoute() {
switch (true) {
//case url.endsWith('/users/authenticate') && method === 'POST':
// return authenticate();
default:
// pass through any requests not handled above
return next.handle(request);
}
}
// route functions
//TODO: FIX THIS
function authenticate() {
const { username, password } = body;
const user = users.find(x => x.username === username && x.password === password);
if (!user) return error('Username or password is incorrect');
return ok({
...basicDetails(user),
token: 'fake-jwt-token'
})
}
function register(){
const user = body
if(users.find(x => x.username === user.username)){
return error('Username"' + user.username + '" is already taken');
}
user.id = users.length ? Math.max(...users.map(x => parseInt(x.id))) + 1 : 1;
users.push(user);
localStorage.setItem(usersKey,JSON.stringify(users));
return ok();
}
function getUsers() {
if (!isLoggedIn()) return unauthorized();
return ok(users.map(x => basicDetails(x)));
}
function getUserById() {
if (!isLoggedIn()) return unauthorized();
const user = users.find(x => parseInt(x.id) === idFromUrl());
return ok(basicDetails(user));
}
function updateUser() {
if (!isLoggedIn()) return unauthorized();
let params = body as User;
let user = users.find(x => parseInt(x.id) === idFromUrl()) as User;
// only update password if entered
if (!params.password) {
delete params.password;
}
// update and save user
Object.assign(user, params);
localStorage.setItem(usersKey, JSON.stringify(users));
return ok();
}
function deleteUser() {
if (!isLoggedIn()) return unauthorized();
users = users.filter(x => parseInt(x.id) !== idFromUrl());
localStorage.setItem(usersKey, JSON.stringify(users));
return ok();
}
// helper functions
function ok(body?: any) {
return of(new HttpResponse({ status: 200, body }))
.pipe(delay(500)); // delay observable to simulate server api call
}
function error(message: string) {
return throwError(() => ({ error: { message } }))
.pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
}
function unauthorized() {
return throwError(() => ({ status: 401, error: { message: 'Unauthorized' } }))
.pipe(materialize(), delay(500), dematerialize());
}
function basicDetails(user: any) {
const { id, username, firstName, lastName, password, admin } = user;
return { id, username, firstName, lastName, password, admin };
}
function isLoggedIn() {
return headers.get('Authorization') === 'Bearer fake-jwt-token';
}
function idFromUrl() {
const urlParts = url.split('/');
return parseInt(urlParts[urlParts.length - 1]);
}
}
}
export const fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true
};

View file

@ -1,4 +1,3 @@
export * from './fake-backend';
export * from './auth.guard';
export * from './jwt.interceptor';
export * from './error.interceptor';

View file

@ -2,7 +2,7 @@
<div class="container">
<h1>Needs</h1>
<a routerLink="add">Add Need</a>
<table class="table table-striped">
<table class="table table-striped" aria-label="Need Data">
<thead>
<tr>
<th>Need Name</th>

View file

@ -9,7 +9,7 @@
</button>
</form>
</div>
<table class="table table-striped">
<table class="table table-striped" aria-label="Needs List">
<thead>
<tr>
<th>Need name</th>

View file

@ -1,7 +1,7 @@
<div>
<div class="container">
<h1>Notifications</h1>
<table class="table table-striped">
<table class="table table-striped" aria-label="Messages Inbox">
<thead>
<tr>
<th>Need</th>
@ -27,7 +27,7 @@
<div class="container">
<h1>Subscriptions</h1>
<table class="table table-striped">
<table class="table table-striped" aria-label="Subscriptions List">
<thead>
<tr>
<th>Need name</th>

View file

@ -2,7 +2,7 @@
<div class="container">
<h1>Users</h1>
<a routerLink="add" class="btn btn-sm btn-success mb-2">Add User</a>
<table class="table table-striped">
<table class="table table-striped" aria-label="Users List">
<thead>
<tr>
<th>First Name</th>
@ -17,15 +17,17 @@
<td>{{user.firstName}}</td>
<td>{{user.lastName}}</td>
<td>{{user.username}}</td>
<td *ngIf="user.hasOwnProperty('admin') && user.admin !== undefined && user.admin === true; else notAdmin">
<input type="checkbox" disabled checked/>
<td
*ngIf="user.hasOwnProperty('admin') && user.admin !== undefined && user.admin === true; else notAdmin">
<input type="checkbox" disabled checked />
</td>
<ng-template #notAdmin>
<input type="checkbox" disabled/>
<input type="checkbox" disabled />
</ng-template>
<td style="white-space: nowrap">
<a routerLink="edit/{{user.id}}" class="btn btn-sm btn-primary">Edit</a>
<button (click)="deleteUser(user.id)" class="btn btn-sm btn-danger" style="width: 58px" [disabled]="user.isDeleting">
<button (click)="deleteUser(user.id)" class="btn btn-sm btn-danger" style="width: 58px"
[disabled]="user.isDeleting">
<span *ngIf="user.isDeleting" class="spinner-border spinner-border-sm"></span>
<span *ngIf="!user.isDeleting">Delete</span>
</button>