Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin - Extend site edit functionality #958

Merged
merged 45 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
15fb04d
Fix file name case
echaidemenos Dec 6, 2023
a61d44c
Update monitoring layout
echaidemenos Dec 6, 2023
3677baa
Merge branch 'master' into monitoring
echaidemenos Dec 7, 2023
8e7c7bf
Add gray to theme variable
echaidemenos Dec 7, 2023
e631b49
Add fetchData as util function
echaidemenos Dec 7, 2023
7732654
Fix consistent casing
echaidemenos Dec 7, 2023
8288285
Improve readability
echaidemenos Dec 7, 2023
65bfbb0
Add undefined check
echaidemenos Dec 7, 2023
5772ae2
Add getTime to improve readability
echaidemenos Dec 7, 2023
27522ea
Add reusable table for monitoring
echaidemenos Dec 7, 2023
bc0bc22
Use MonitoringTable for MonthlyReport
echaidemenos Dec 8, 2023
8bf1637
Add monitoring Surveys Report page
echaidemenos Dec 8, 2023
a4ca548
Add noopener
echaidemenos Dec 8, 2023
d405b5a
Merge branch 'monitoring-table' into monitoring-surveys-report
echaidemenos Dec 8, 2023
607c8f4
Merge branch 'master' into monitoring-table
ericboucher Dec 8, 2023
5c7f2c0
Merge branch 'monitoring-table' into monitoring-surveys-report
ericboucher Dec 8, 2023
8c71089
Add comments on stableSort
echaidemenos Dec 11, 2023
493d31a
Merge branch 'monitoring-table' into monitoring-surveys-report
echaidemenos Dec 11, 2023
6220696
Add tests
echaidemenos Dec 11, 2023
e650282
Update tests
echaidemenos Dec 11, 2023
3f8478e
merge branch 'master' into monitoring-surveys-report
echaidemenos Dec 11, 2023
da1c0be
Add sites overview page
echaidemenos Dec 11, 2023
67f3e92
Merge branch 'monitoring-surveys-report' into monitoring-application-…
ericboucher Dec 11, 2023
371df9e
Add page title
echaidemenos Dec 12, 2023
0fea845
Merge branch 'monitoring-surveys-report' into monitoring-application-…
echaidemenos Dec 12, 2023
3b4174f
Rename SitesOverview
echaidemenos Dec 12, 2023
6042ac0
Fix column names
echaidemenos Dec 12, 2023
35fba6e
Merge branch 'master' into monitoring-application-admin
echaidemenos Dec 12, 2023
32d0e18
Add filters
echaidemenos Dec 12, 2023
ce4ac5c
Add email validation
echaidemenos Dec 14, 2023
6c99a68
Add status selector
echaidemenos Dec 14, 2023
a143e6d
Remove date formating in Surveys Report
echaidemenos Dec 14, 2023
116ea5e
Add contact info column and extend site edit
echaidemenos Dec 14, 2023
6d5d544
Fix tests
echaidemenos Dec 14, 2023
c9488b7
Add site status page
echaidemenos Dec 15, 2023
90eaac8
Merge branch 'master' into site-edit
echaidemenos Dec 15, 2023
25c2355
update column names
ericboucher Dec 16, 2023
a8d46fe
Update index.tsx
ericboucher Dec 16, 2023
8785bcf
Use ilike in query
ericboucher Dec 16, 2023
a0aa1ef
Update index.test.tsx.snap
ericboucher Dec 16, 2023
3ea4834
Update get-sites-overview.dto.ts
ericboucher Dec 16, 2023
a81cd25
Update monitoring.service.ts
ericboucher Dec 16, 2023
a29d3ab
use ilike option
ericboucher Dec 16, 2023
1987aeb
escape and fix username typo
ericboucher Dec 17, 2023
212bf2e
Update monitoring.service.ts
ericboucher Dec 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddContactInformationColumn1702565409448
implements MigrationInterface
{
name = 'AddContactInformationColumn1702565409448';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "site" ADD "contact_information" character varying`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "site" DROP COLUMN "contact_information"`,
);
}
}
21 changes: 19 additions & 2 deletions packages/api/src/collections/collections.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const collectionTests = () => {
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);
expect(sortedSites[1]).toMatchObject(
Expand All @@ -186,6 +187,7 @@ export const collectionTests = () => {
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);

Expand Down Expand Up @@ -231,7 +233,14 @@ export const collectionTests = () => {

const sortedSites = sortBy(rsp.body.sites, (o) => o.name);
expect(sortedSites[0]).toMatchObject(
omit(athensSite, 'applied', 'createdAt', 'updatedAt', 'spotterApiToken'),
omit(
athensSite,
'applied',
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);
expect(sortedSites[1]).toMatchObject(
omit(
Expand All @@ -240,10 +249,18 @@ export const collectionTests = () => {
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);
expect(sortedSites[2]).toMatchObject(
omit(floridaSite, 'applied', 'createdAt', 'updatedAt', 'spotterApiToken'),
omit(
floridaSite,
'applied',
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);

const athensLatestData = getLatestData(athensTimeSeries);
Expand Down
11 changes: 2 additions & 9 deletions packages/api/src/monitoring/dto/get-sites-overview.dto.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsEmail,
IsEnum,
IsNumber,
IsOptional,
Validate,
} from 'class-validator';
import { IsEmail, IsEnum, IsOptional, Validate } from 'class-validator';
import { Site, SiteStatus } from 'sites/sites.entity';
import { EntityExists } from 'validations/entity-exists.constraint';

export class GetSitesOverview {
export class GetSitesOverviewDto {
@ApiProperty({ example: 42 })
@IsOptional()
@IsNumber()
@Validate(EntityExists, [Site])
siteId?: number;

Expand Down
10 changes: 9 additions & 1 deletion packages/api/src/monitoring/monitoring.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Auth } from 'auth/auth.decorator';
import { AuthRequest } from 'auth/auth.types';
import { AdminLevel } from 'users/users.entity';
import { GetMonitoringStatsDto } from './dto/get-monitoring-stats.dto';
import { GetSitesOverviewDto } from './dto/get-sites-overview.dto';
import { PostMonitoringMetricDto } from './dto/post-monitoring-metric.dto';
import { MonitoringService } from './monitoring.service';

Expand Down Expand Up @@ -58,7 +59,14 @@ export class MonitoringController {
@Get('sites-overview')
@ApiOperation({ summary: 'Get Aqualink overview' })
@Auth(AdminLevel.SuperAdmin)
getSitesOverview(@Query() getSitesOverviewDto: GetMonitoringStatsDto) {
getSitesOverview(@Query() getSitesOverviewDto: GetSitesOverviewDto) {
return this.monitoringService.SitesOverview(getSitesOverviewDto);
}

@Get('sites-status')
@ApiOperation({ summary: "Get sites' status" })
@Auth(AdminLevel.SuperAdmin)
getSitesStatus() {
return this.monitoringService.getSitesStatus();
}
}
33 changes: 27 additions & 6 deletions packages/api/src/monitoring/monitoring.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { LatestData } from 'time-series/latest-data.entity';
import { IsNull, Not, Repository } from 'typeorm';
import { AdminLevel, User } from 'users/users.entity';
import { getDefaultDates } from 'utils/dates';
import { GetSitesOverview } from './dto/get-sites-overview.dto';
import { GetSitesOverviewDto } from './dto/get-sites-overview.dto';
import { GetMonitoringStatsDto } from './dto/get-monitoring-stats.dto';
import { PostMonitoringMetricDto } from './dto/post-monitoring-metric.dto';
import { Monitoring } from './monitoring.entity';
Expand Down Expand Up @@ -172,7 +172,7 @@ export class MonitoringService {
: null;

if (spotterSite === null && spotterId) {
throw new BadRequestException('Invalid parameter: spotterId');
throw new BadRequestException('Invalid value for parameter: spotterId');
}

const querySiteIds = siteIds || [spotterSite!.id];
Expand Down Expand Up @@ -244,7 +244,7 @@ export class MonitoringService {
adminUsername,
organization,
status,
}: GetSitesOverview) {
}: GetSitesOverviewDto) {
const latestDataSubQuery = this.latestDataRepository
.createQueryBuilder('latest_data')
.select(
Expand All @@ -257,7 +257,7 @@ export class MonitoringService {
const surveysCountSubQuery = this.surveyRepository
.createQueryBuilder('survey')
.select('survey.site_id', 'site_id')
.addSelect('COUNT(*)', 'surveysCount')
.addSelect('COUNT(*)', 'count')
.groupBy('survey.site_id');

const baseQuery = this.siteRepository
Expand All @@ -273,7 +273,8 @@ export class MonitoringService {
.addSelect('site.video_stream', 'videoStream')
.addSelect('site.updated_at', 'updatedAt')
.addSelect('latest_data.timestamp', 'lastDataReceived')
.addSelect('COALESCE("surveys_count"."surveysCount", 0)', 'surveysCount')
.addSelect('COALESCE(surveys_count.count, 0)', 'surveysCount')
.addSelect('site.contact_information', 'contactInformation')
.leftJoin(
'users_administered_sites_site',
'uass',
Expand Down Expand Up @@ -332,8 +333,28 @@ export class MonitoringService {
.addGroupBy('site.video_stream')
.addGroupBy('site.updated_at')
.addGroupBy('latest_data.timestamp')
.addGroupBy('"surveys_count"."surveysCount"');
.addGroupBy('surveys_count.count')
.addGroupBy('site.contact_information');

return ret.getRawMany();
}

getSitesStatus() {
return this.siteRepository
.createQueryBuilder('site')
.select('COUNT(*)', 'totalSites')
.addSelect("COUNT(*) FILTER (WHERE site.status = 'deployed')", 'deployed')
.addSelect('COUNT(*) FILTER (WHERE site.display)', 'displayed')
.addSelect(
"COUNT(*) FILTER (WHERE site.status = 'maintenance')",
'maintenance',
)
.addSelect("COUNT(*) FILTER (WHERE site.status = 'shipped')", 'shipped')
.addSelect(
"COUNT(*) FILTER (WHERE site.status = 'end_of_life')",
'endOfLife',
)
.addSelect("COUNT(*) FILTER (WHERE site.status = 'lost')", 'lost')
.getRawOne();
}
}
18 changes: 18 additions & 0 deletions packages/api/src/monitoring/monitoring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,23 @@ export const monitoringTests = () => {
expect(rsp.body[0].updatedAt).toBeDefined();
expect(rsp.body[0].lastDataReceived).toBeDefined();
expect(rsp.body[0].surveysCount).toBeDefined();
expect(rsp.body[0].contactInformation).toBeDefined();
});

it('GET /sites-status', async () => {
mockExtractAndVerifyToken(adminFirebaseUserMock);
const rsp = await request(app.getHttpServer())
.get('/monitoring/sites-status')
.send();

expect(rsp.status).toBe(200);

expect(rsp.body.totalSites).toBeDefined();
expect(rsp.body.deployed).toBeDefined();
expect(rsp.body.displayed).toBeDefined();
expect(rsp.body.maintenance).toBeDefined();
expect(rsp.body.shipped).toBeDefined();
expect(rsp.body.endOfLife).toBeDefined();
expect(rsp.body.lost).toBeDefined();
});
};
24 changes: 22 additions & 2 deletions packages/api/src/sites/dto/update-site.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
IsLongitude,
IsObject,
ValidateNested,
IsEnum,
} from 'class-validator';
import { Type } from 'class-transformer';
import { Transform, Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { SiteStatus } from 'sites/sites.entity';
import { EntityExists } from '../../validations/entity-exists.constraint';
import { Region } from '../../regions/regions.entity';
import { User } from '../../users/users.entity';
Expand Down Expand Up @@ -49,7 +51,7 @@ export class UpdateSiteDto {

@IsOptional()
@IsUrl()
readonly videoStream?: string;
readonly videoStream?: string | null;

@ApiProperty({ example: 1 })
@IsOptional()
Expand Down Expand Up @@ -82,4 +84,22 @@ export class UpdateSiteDto {
@IsNotEmpty()
@MaxLength(100)
readonly spotterApiToken?: string | null;

@ApiProperty({ example: 'deployed' })
@IsOptional()
@IsEnum(SiteStatus)
readonly status?: SiteStatus;

@IsOptional()
@Transform(({ value }) => {
return [true, 'true', 1, '1'].indexOf(value) > -1;
})
readonly display?: boolean;

@ApiProperty({ example: 'email: [email protected]' })
@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(100)
readonly contactInformation?: string | null;
}
14 changes: 13 additions & 1 deletion packages/api/src/sites/sites.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ export class SitesController {
update(
@Param('siteId', ParseIntPipe) id: number,
@Body() updateSiteDto: UpdateSiteDto,
@Req() request: AuthRequest,
): Promise<Site> {
return this.sitesService.update(id, updateSiteDto);
return this.sitesService.update(id, updateSiteDto, request.user);
}

@ApiBearerAuth()
Expand Down Expand Up @@ -196,4 +197,15 @@ export class SitesController {
): Promise<ExclusionDates[]> {
return this.sitesService.getExclusionDates(id);
}

@ApiBearerAuth()
@ApiOperation({
summary: 'Returns sites contact information notes',
})
@ApiParam({ name: 'siteId', example: 1 })
@Auth(AdminLevel.SuperAdmin)
@Get(':siteId/contact_info')
getContactInformation(@Param('siteId', ParseIntPipe) id: number) {
return this.sitesService.getContactInformation(id);
}
}
5 changes: 5 additions & 0 deletions packages/api/src/sites/sites.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export class Site {
@Column({ nullable: true, select: false, type: 'character varying' })
spotterApiToken?: string | null;

@ApiHideProperty()
@Exclude()
@Column({ nullable: true, select: false, type: 'character varying' })
contactInformation?: string | null;

hasHobo: boolean;

collectionData?: CollectionDataDto;
Expand Down
28 changes: 27 additions & 1 deletion packages/api/src/sites/sites.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Logger,
BadRequestException,
ConflictException,
ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
Expand Down Expand Up @@ -235,7 +236,21 @@ export class SitesService {
};
}

async update(id: number, updateSiteDto: UpdateSiteDto): Promise<Site> {
async update(
id: number,
updateSiteDto: UpdateSiteDto,
user: User,
): Promise<Site> {
if (
(updateSiteDto.display ||
updateSiteDto.status ||
updateSiteDto.videoStream ||
updateSiteDto.contactInformation) &&
user.adminLevel !== AdminLevel.SuperAdmin
) {
throw new ForbiddenException();
}

const { coordinates, adminIds, regionId, streamId } = updateSiteDto;
const updateRegion =
regionId !== undefined ? { region: { id: regionId } } : {};
Expand Down Expand Up @@ -567,4 +582,15 @@ export class SitesService {
// All checks passed, return video stream url.
return site.videoStream;
}

async getContactInformation(siteId: number) {
const { contactInformation } = await getSite(
siteId,
this.sitesRepository,
undefined,
true,
);

return { contactInformation };
}
}
9 changes: 8 additions & 1 deletion packages/api/src/sites/sites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,13 @@ export const siteTests = () => {
expect(rsp.body.length).toBe(1);

expect(rsp.body[0]).toMatchObject(
omit(floridaSite, 'createdAt', 'updatedAt', 'spotterApiToken'),
omit(
floridaSite,
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);
});

Expand All @@ -242,6 +248,7 @@ export const siteTests = () => {
'createdAt',
'updatedAt',
'spotterApiToken',
'contactInformation',
),
);
});
Expand Down
16 changes: 14 additions & 2 deletions packages/api/src/users/users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,22 @@ export const userTests = () => {
expect(rsp.status).toBe(200);
expect(rsp.body.length).toBe(2);
expect(rsp.body[0]).toMatchObject({
...omit(californiaSite, 'updatedAt', 'createdAt', 'spotterApiToken'),
...omit(
californiaSite,
'updatedAt',
'createdAt',
'spotterApiToken',
'contactInformation',
),
});
expect(rsp.body[1]).toMatchObject({
...omit(floridaSite, 'updatedAt', 'createdAt', 'spotterApiToken'),
...omit(
floridaSite,
'updatedAt',
'createdAt',
'spotterApiToken',
'contactInformation',
),
});
});
});
Expand Down
Loading
Loading