Skip to content

Commit

Permalink
feat(admin): home dashboard with stats
Browse files Browse the repository at this point in the history
  • Loading branch information
Ealenn committed Jun 25, 2022
1 parent a319da4 commit 38ca25f
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 19 deletions.
5 changes: 4 additions & 1 deletion admin/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HttpClient } from "./configs/HttpClient";
import { BASE_URL } from "./configs/BaseUrl";

import { OpenGardenAdminLayout } from './themes/Layout';
import Dashboard from "./Dashboard";

import { TbSeeding, TbTree, TbPlant2, TbStar } from 'react-icons/tb';

Expand All @@ -20,7 +21,9 @@ const App = () => (
requireAuth={true}
layout={OpenGardenAdminLayout}
authProvider={AuthProvider}
dataProvider={Provider(BASE_URL, HttpClient)}>
dataProvider={Provider(BASE_URL, HttpClient)}
dashboard={Dashboard}
disableTelemetry>
{permissions => (
<>
<Resource name="plants"
Expand Down
66 changes: 66 additions & 0 deletions admin/src/Dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable import/no-anonymous-default-export */
import * as React from 'react';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import { TbUser, TbSeeding, TbTree, TbPlant2 } from 'react-icons/tb';
import { HttpClient } from './configs/HttpClient';
import { BASE_URL } from './configs/BaseUrl';

export default class Dashboard extends React.Component {
state = {
loading: true,
stats: {}
};

componentDidMount() {
HttpClient(`${BASE_URL}/stats/global`).then(res => {
this.setState({ stats: JSON.parse(res.body), loading: false })
});
};

render() {
return (
<>
<Box component="span" sx={{ p: 5 }}>
<Grid container spacing={2}>
<Grid item xs={3}>
<Card sx={{ minWidth: 275 }} variant="outlined">
<CardHeader
avatar={<TbUser />}
title="Users"
subheader={this.state.loading ? <Skeleton /> : this.state?.stats?.estimatedCount?.users}></CardHeader>
</Card>
</Grid>
<Grid item xs={3}>
<Card sx={{ minWidth: 275 }} variant="outlined">
<CardHeader
avatar={<TbTree />}
title="Plants"
subheader={this.state.loading ? <Skeleton /> : this.state?.stats?.estimatedCount?.plants}></CardHeader>
</Card>
</Grid>
<Grid item xs={3}>
<Card sx={{ minWidth: 275 }} variant="outlined">
<CardHeader
avatar={<TbSeeding />}
title="Varieties"
subheader={this.state.loading ? <Skeleton /> : this.state?.stats?.estimatedCount?.varieties}></CardHeader>
</Card>
</Grid>
<Grid item xs={3}>
<Card sx={{ minWidth: 275 }} variant="outlined">
<CardHeader
avatar={<TbPlant2 />}
title="Floors"
subheader={this.state.loading ? <Skeleton /> : this.state?.stats?.estimatedCount?.floors}></CardHeader>
</Card>
</Grid>
</Grid>
</Box>
</>
);
}
}
2 changes: 2 additions & 0 deletions core/src/controllers/controllers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FavoritesVarietiesController } from './favorites/favorites.varieties.co
import { VarietyExistsRule } from './varieties/constraint/variety.exists.rule';
import { FavoriteVarietiesMapperProfiles } from './favorites/models/mapper.profiles';
import { RootController } from './root.controller';
import { StatsController } from './stats/stats.controller';

@Module({
imports: [
Expand Down Expand Up @@ -53,6 +54,7 @@ import { RootController } from './root.controller';
VarietiesController,
FloorsController,
FavoritesVarietiesController,
StatsController,
],
})
export class ControllersModule {}
20 changes: 20 additions & 0 deletions core/src/controllers/stats/models/stats.global.response.body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';

export class StatsGlobalCountResponseBody {
@ApiProperty()
users: number;

@ApiProperty()
plants: number;

@ApiProperty()
varieties: number;

@ApiProperty()
floors: number;
}

export class StatsGlobalResponseBody {
@ApiProperty({ type: StatsGlobalCountResponseBody })
estimatedCount: StatsGlobalCountResponseBody;
}
35 changes: 35 additions & 0 deletions core/src/controllers/stats/stats.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Controller, Get } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiResponse } from '@nestjs/swagger';
import { VarietiesService } from '../../entities/varieties/varieties.service';
import { UsersService } from '../../users/users.service';
import { PlantsService } from '../../entities/plants/plants.service';
import { FloorsService } from '../../entities/floors/floors.service';
import { StatsGlobalResponseBody } from './models/stats.global.response.body';

@ApiBearerAuth()
@ApiTags('Stats')
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 429, description: 'Too Many Requests' })
@Controller('stats')
export class StatsController {
constructor(
private usersService: UsersService,
private plantsService: PlantsService,
private varietiesService: VarietiesService,
private floorsService: FloorsService,
) {}

@Get('global')
@ApiResponse({ status: 200, type: StatsGlobalResponseBody })
async getGlobal(): Promise<StatsGlobalResponseBody> {
return {
estimatedCount: {
users: await this.usersService.count(),
plants: await this.plantsService.count(),
varieties: await this.varietiesService.count(),
floors: await this.floorsService.count(),
},
};
}
}
7 changes: 5 additions & 2 deletions core/src/entities/favorites/models/favorite.variety.entity.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { User } from '../../../users/models/user.entity';
import { Variety, VarietySchema } from '../../../entities/varieties/models/variety.entity';
import { BaseEntity } from '../../base.entity';

export type FavoriteVarietyDocument = FavoriteVariety & Document;

@Schema()
export class FavoriteVariety extends BaseEntity {
@Prop({ required: true, type: [{ type: mongoose.Schema.Types.ObjectId, ref: User.name }] })
@Prop({
required: true,
type: [{ type: mongoose.Schema.Types.ObjectId, ref: Variety.name, childSchemas: VarietySchema }],
})
variety: mongoose.Types.ObjectId;

@Prop({ required: true, unique: true, index: true })
Expand Down
16 changes: 10 additions & 6 deletions core/src/entities/floors/floors.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ export class FloorSearchParams extends BaseSearchParams {

@Injectable()
export class FloorsService extends BaseEntityService {
constructor(@InjectModel(Floor.name) private FloorModel: Model<FloorDocument>) {
constructor(@InjectModel(Floor.name) private floorModel: Model<FloorDocument>) {
super();
}

async create(Floor: Floor): Promise<Floor> {
try {
Floor._id = new mongoose.Types.ObjectId();
return await this.FloorModel.create(Floor);
return await this.floorModel.create(Floor);
} catch (exception) {
if (exception.code === 11000) {
throw new ConflictException();
Expand All @@ -30,22 +30,26 @@ export class FloorsService extends BaseEntityService {

async deleteFloor(floorId: string): Promise<Floor | undefined> {
try {
return await this.FloorModel.findByIdAndDelete(floorId);
return await this.floorModel.findByIdAndDelete(floorId);
} catch {
return null;
}
}

async findOneById(id: string): Promise<Floor | undefined> {
return await this.FloorModel.findById(id);
return await this.floorModel.findById(id);
}

async search(params: FloorSearchParams): Promise<[Floor[], number]> {
const { pagination, ...filters } = params;
const findParam = this._generateFilters(filters);

const elements = await this.FloorModel.find(findParam).skip(pagination.offset).limit(pagination.limit);
const count = await this.FloorModel.count(findParam);
const elements = await this.floorModel.find(findParam).skip(pagination.offset).limit(pagination.limit);
const count = await this.floorModel.count(findParam);
return [elements, count];
}

async count(): Promise<number> {
return await this.floorModel.estimatedDocumentCount();
}
}
16 changes: 10 additions & 6 deletions core/src/entities/plants/plants.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export class PlantSearchParams extends BaseSearchParams {

@Injectable()
export class PlantsService extends BaseEntityService {
constructor(@InjectModel(Plant.name) private PlantModel: Model<PlantDocument>) {
constructor(@InjectModel(Plant.name) private plantModel: Model<PlantDocument>) {
super();
}

async create(Plant: Plant): Promise<Plant> {
try {
Plant._id = new mongoose.Types.ObjectId();
return await this.PlantModel.create(Plant);
return await this.plantModel.create(Plant);
} catch (exception) {
if (exception.code === 11000) {
throw new ConflictException();
Expand All @@ -36,14 +36,14 @@ export class PlantsService extends BaseEntityService {

async deletePlant(plantId: string): Promise<Plant | undefined> {
try {
return await this.PlantModel.findByIdAndDelete(plantId);
return await this.plantModel.findByIdAndDelete(plantId);
} catch {
return null;
}
}

async findOneById(id: string): Promise<Plant | undefined> {
return await this.PlantModel.findById(id);
return await this.plantModel.findById(id);
}

async search(params: PlantSearchParams): Promise<[Plant[], number]> {
Expand All @@ -56,8 +56,12 @@ export class PlantsService extends BaseEntityService {
findParam = this._generateFilter(findParam, 'classification.species', species);
findParam = this._generateFilter(findParam, 'classification.binomialName', binomialName);

const elements = await this.PlantModel.find(findParam).skip(pagination.offset).limit(pagination.limit);
const count = await this.PlantModel.count(findParam);
const elements = await this.plantModel.find(findParam).skip(pagination.offset).limit(pagination.limit);
const count = await this.plantModel.count(findParam);
return [elements, count];
}

async count(): Promise<number> {
return await this.plantModel.estimatedDocumentCount();
}
}
4 changes: 2 additions & 2 deletions core/src/entities/varieties/models/requirement.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Prop } from '@nestjs/mongoose';
import mongoose from 'mongoose';
import { Floor } from '../../floors/models/floor.entity';
import { Floor, FloorSchema } from '../../floors/models/floor.entity';

export enum VarietyRequirementWaterNeed {
LOW = 'LOW',
Expand Down Expand Up @@ -37,6 +37,6 @@ export class VarietyRequirement {
@Prop({ required: true })
sun: VarietyRequirementSun;

@Prop({ required: true, type: [{ type: mongoose.Schema.Types.ObjectId, ref: Floor.name }] })
@Prop({ required: true, type: [{ type: Array<mongoose.Schema.Types.ObjectId>, ref: Floor.name, childSchemas: FloorSchema }] })
floors: mongoose.Types.ObjectId[];
}
7 changes: 5 additions & 2 deletions core/src/entities/varieties/models/variety.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { Plant } from '../../plants/models/plant.entity';
import { Plant, PlantSchema } from '../../plants/models/plant.entity';
import { BasePublishedEntity } from '../../base.published.entity';
import { VarietyRequirement } from './requirement.entity';
import { VarietyCulture } from './culture.entity';
Expand All @@ -15,7 +15,10 @@ export enum VarietyPrecocity {

@Schema()
export class Variety extends BasePublishedEntity {
@Prop({ required: true, type: [{ type: mongoose.Schema.Types.ObjectId, ref: Plant.name }] })
@Prop({
required: true,
type: [{ type: mongoose.Schema.Types.ObjectId, ref: Plant.name, childSchemas: PlantSchema }],
})
plant: mongoose.Types.ObjectId;

@Prop({ required: true, unique: true })
Expand Down
4 changes: 4 additions & 0 deletions core/src/entities/varieties/varieties.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@ export class VarietiesService extends BaseEntityService {
const count = await this.varietyModel.count(findParam);
return [elements, count];
}

async count(): Promise<number> {
return await this.varietyModel.estimatedDocumentCount();
}
}
4 changes: 4 additions & 0 deletions core/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ export class UsersService {
async findOneById(id: string): Promise<User | undefined> {
return await this.userModel.findOne({ id });
}

async count(): Promise<number> {
return await this.userModel.estimatedDocumentCount();
}
}

0 comments on commit 38ca25f

Please sign in to comment.