-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api-service,dashboard): New subscribers page and api
- Loading branch information
Showing
22 changed files
with
483 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
apps/api/src/app/subscribers-v2/dtos/subscriber-pagination.dto.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { DirectionEnum, SubscriberPaginationDto } from '@novu/shared'; | ||
import { Type } from 'class-transformer'; | ||
import { IsEnum, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; | ||
|
||
export class SubscriberPaginationRequestDto implements SubscriberPaginationDto { | ||
@ApiProperty({ | ||
description: 'Number of items per page', | ||
required: false, | ||
default: 10, | ||
maximum: 100, | ||
}) | ||
@IsNumber() | ||
@Min(1) | ||
@Max(100) | ||
@IsOptional() | ||
@Type(() => Number) | ||
limit: number = 10; | ||
|
||
@ApiProperty({ | ||
description: 'Cursor for pagination', | ||
required: false, | ||
}) | ||
@IsString() | ||
@IsOptional() | ||
cursor?: string; | ||
|
||
@ApiProperty({ | ||
description: 'Sort direction', | ||
required: false, | ||
enum: DirectionEnum, | ||
default: DirectionEnum.DESC, | ||
}) | ||
@IsEnum(DirectionEnum) | ||
@IsOptional() | ||
orderDirection?: DirectionEnum = DirectionEnum.DESC; | ||
|
||
@ApiProperty({ | ||
description: 'Field to order by', | ||
required: false, | ||
enum: ['updatedAt', 'createdAt', 'lastOnlineAt'], | ||
default: 'createdAt', | ||
}) | ||
@IsEnum(['updatedAt', 'createdAt', 'lastOnlineAt']) | ||
@IsOptional() | ||
orderBy?: 'updatedAt' | 'createdAt' | 'lastOnlineAt' = 'createdAt'; | ||
|
||
@ApiProperty({ | ||
description: 'Search query to filter subscribers by name, email, phone, or subscriberId', | ||
required: false, | ||
}) | ||
@IsString() | ||
@IsOptional() | ||
query?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { Controller, Get, Query, UseGuards, UseInterceptors } from '@nestjs/common'; | ||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; | ||
import { ClassSerializerInterceptor } from '@nestjs/common'; | ||
import { UserSession, UserAuthGuard } from '@novu/application-generic'; | ||
import { ApiCommonResponses } from '../shared/framework/response.decorator'; | ||
import { DirectionEnum, ListSubscriberResponse, UserSessionData } from '@novu/shared'; | ||
|
||
import { ListSubscribersCommand } from './usecases/list-subscribers/list-subscribers.command'; | ||
import { ListSubscribersUseCase } from './usecases/list-subscribers/list-subscribers.usecase'; | ||
import { SubscriberPaginationRequestDto } from './dtos/subscriber-pagination.dto'; | ||
|
||
@Controller({ path: '/subscribers', version: '2' }) | ||
@UseInterceptors(ClassSerializerInterceptor) | ||
@UseGuards(UserAuthGuard) | ||
@ApiTags('Subscribers') | ||
@ApiCommonResponses() | ||
export class SubscriberController { | ||
constructor(private listSubscribersUsecase: ListSubscribersUseCase) {} | ||
|
||
@Get('') | ||
@ApiOperation({ | ||
summary: 'List subscribers', | ||
description: 'Returns a paginated list of subscribers', | ||
}) | ||
@ApiResponse({ | ||
status: 200, | ||
description: 'A list of subscribers with pagination information', | ||
type: ListSubscriberResponse, | ||
}) | ||
async getSubscribers( | ||
@UserSession() user: UserSessionData, | ||
@Query() query: SubscriberPaginationRequestDto | ||
): Promise<ListSubscriberResponse> { | ||
return await this.listSubscribersUsecase.execute( | ||
ListSubscribersCommand.create({ | ||
environmentId: user.environmentId, | ||
organizationId: user.organizationId, | ||
limit: query.limit || 10, | ||
cursor: query.cursor, | ||
orderDirection: query.orderDirection || DirectionEnum.DESC, | ||
orderBy: query.orderBy || 'createdAt', | ||
query: query.query, | ||
}) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { SharedModule } from '../shared/shared.module'; | ||
import { SubscriberController } from './subscriber.controller'; | ||
import { ListSubscribersUseCase } from './usecases/list-subscribers/list-subscribers.usecase'; | ||
|
||
const USE_CASES = [ListSubscribersUseCase]; | ||
|
||
@Module({ | ||
imports: [SharedModule], | ||
controllers: [SubscriberController], | ||
providers: [...USE_CASES], | ||
}) | ||
export class SubscriberModule {} |
26 changes: 26 additions & 0 deletions
26
apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.command.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { IsNumber, IsOptional, IsString, Max, Min, IsEnum } from 'class-validator'; | ||
import { EnvironmentCommand } from '../../../shared/commands/project.command'; | ||
import { DirectionEnum } from '@novu/shared'; | ||
|
||
export class ListSubscribersCommand extends EnvironmentCommand { | ||
@IsNumber() | ||
@Min(1) | ||
@Max(100) | ||
limit: number; | ||
|
||
@IsString() | ||
@IsOptional() | ||
cursor?: string; | ||
|
||
@IsEnum(DirectionEnum) | ||
@IsOptional() | ||
orderDirection: DirectionEnum = DirectionEnum.DESC; | ||
|
||
@IsEnum(['updatedAt', 'createdAt', 'lastOnlineAt']) | ||
@IsOptional() | ||
orderBy: 'updatedAt' | 'createdAt' | 'lastOnlineAt' = 'createdAt'; | ||
|
||
@IsString() | ||
@IsOptional() | ||
query?: string; | ||
} |
58 changes: 58 additions & 0 deletions
58
apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.usecase.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { SubscriberRepository } from '@novu/dal'; | ||
import { InstrumentUsecase } from '@novu/application-generic'; | ||
import { DirectionEnum, ISubscriberResponseDto, ListSubscriberResponse } from '@novu/shared'; | ||
import { ListSubscribersCommand } from './list-subscribers.command'; | ||
|
||
@Injectable() | ||
export class ListSubscribersUseCase { | ||
constructor(private subscriberRepository: SubscriberRepository) {} | ||
|
||
@InstrumentUsecase() | ||
async execute(command: ListSubscribersCommand): Promise<ListSubscriberResponse> { | ||
const query = { | ||
_environmentId: command.environmentId, | ||
_organizationId: command.organizationId, | ||
} as const; | ||
|
||
if (command.query) { | ||
Object.assign(query, { | ||
$or: [ | ||
{ subscriberId: { $regex: command.query, $options: 'i' } }, | ||
{ email: { $regex: command.query, $options: 'i' } }, | ||
{ phone: { $regex: command.query, $options: 'i' } }, | ||
{ | ||
$expr: { | ||
$regexMatch: { | ||
input: { $concat: ['$firstName', ' ', '$lastName'] }, | ||
regex: command.query, | ||
options: 'i', | ||
}, | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
|
||
if (command.cursor) { | ||
const operator = command.orderDirection === DirectionEnum.ASC ? '$gt' : '$lt'; | ||
Object.assign(query, { | ||
[command.orderBy]: { [operator]: command.cursor }, | ||
}); | ||
} | ||
|
||
const subscribers = await this.subscriberRepository.find(query, undefined, { | ||
limit: command.limit + 1, // Get one extra to determine if there are more items | ||
sort: { [command.orderBy]: command.orderDirection === DirectionEnum.ASC ? 1 : -1 }, | ||
}); | ||
|
||
const hasMore = subscribers.length > command.limit; | ||
const data = hasMore ? subscribers.slice(0, -1) : subscribers; | ||
|
||
return { | ||
subscribers: data, | ||
hasMore, | ||
pageSize: command.limit, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { IEnvironment, ListSubscriberResponse } from '@novu/shared'; | ||
import { getV2 } from './api.client'; | ||
|
||
export const getSubscribers = async ({ | ||
environment, | ||
cursor, | ||
limit, | ||
query, | ||
}: { | ||
environment: IEnvironment; | ||
cursor: string; | ||
query: string; | ||
limit: number; | ||
}): Promise<ListSubscriberResponse> => { | ||
const { data } = await getV2<{ data: ListSubscriberResponse }>( | ||
`/subscribers?cursor=${cursor}&limit=${limit}&query=${query}`, | ||
{ | ||
environment, | ||
} | ||
); | ||
return data; | ||
}; |
25 changes: 25 additions & 0 deletions
25
apps/dashboard/src/components/icons/add-subscriber-illustration.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { HTMLAttributes } from 'react'; | ||
|
||
type AddSubscriberIllustrationProps = HTMLAttributes<HTMLOrSVGElement>; | ||
export const AddSubscriberIllustration = (props: AddSubscriberIllustrationProps) => { | ||
return ( | ||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="126" fill="none" {...props}> | ||
<rect width="135" height="45" x="1" y="80" stroke="#CACFD8" strokeDasharray="5 3" rx="7.5" /> | ||
<rect width="127" height="37" x="5" y="84" fill="#fff" rx="5.5" /> | ||
<rect width="127" height="37" x="5" y="84" stroke="#F2F5F8" rx="5.5" /> | ||
<path fill="#99A0AE" d="M68.125 102.125v-2.25h.75v2.25h2.25v.75h-2.25v2.25h-.75v-2.25h-2.25v-.75h2.25Z" /> | ||
<rect width="135" height="45" x="1" y="1" stroke="#DD2450" rx="7.5" /> | ||
<rect width="128" height="38" x="4.5" y="4.5" fill="#fff" rx="6" /> | ||
<rect width="127" height="37" x="5" y="5" stroke="#FB3748" strokeOpacity=".24" rx="5.5" /> | ||
|
||
<g transform="translate(60, 15)"> | ||
<path | ||
fill="#D82651" | ||
d="M7.03 8.2a1.35 1.35 0 1 1 0-2.7 1.35 1.35 0 0 1 0 2.7Zm.27 4.949V11.14c0-.293.086-.562.242-.803a3.883 3.883 0 0 0-3.02.85A4.807 4.807 0 0 0 7.3 13.15Zm-3.328-3.053A5.077 5.077 0 0 1 7 9.1c.626 0 1.226.113 1.78.32a5.057 5.057 0 0 1 1.82-.32c.996 0 1.911.254 2.524.694a4.8 4.8 0 1 0-9.152.302Zm8.655.856c-.235-.32-1.024-.652-2.027-.652-1.204 0-2.1.478-2.1.84v2.16a4.797 4.797 0 0 0 4.128-2.348ZM8.5 14.5a6 6 0 1 1 0-12 6 6 0 0 1 0 12Zm2.1-5.7a1.2 1.2 0 1 1 0-2.4 1.2 1.2 0 0 1 0 2.4Z" | ||
/> | ||
</g> | ||
|
||
<path stroke="#CACFD8" strokeDasharray="5 3" strokeLinejoin="bevel" strokeWidth="1.33" d="M68.5 49.665v26.67" /> | ||
</svg> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { AddSubscriberIllustration } from '@/components/icons/add-subscriber-illustration'; | ||
import { RiBookMarkedLine } from 'react-icons/ri'; | ||
import { Link } from 'react-router-dom'; | ||
import { LinkButton } from './primitives/button-link'; | ||
|
||
export const SubscriberListEmpty = () => { | ||
return ( | ||
<div className="flex h-full w-full flex-col items-center justify-center gap-6"> | ||
<AddSubscriberIllustration /> | ||
<div className="flex flex-col items-center gap-2 text-center"> | ||
<span className="text-foreground-900 block font-medium">No subscribers yet</span> | ||
<p className="text-foreground-400 max-w-[60ch] text-sm"> | ||
A Subscriber is a unique entity for receiving notifications. Add them ahead of time or migrate them | ||
dynamically when sending notifications. | ||
</p> | ||
</div> | ||
|
||
<div className="flex items-center justify-center gap-6"> | ||
<Link to={'https://docs.novu.co/concepts/subscribers#migration-optional'} target="_blank"> | ||
<LinkButton variant="gray" trailingIcon={RiBookMarkedLine}> | ||
Migrate via API | ||
</LinkButton> | ||
</Link> | ||
|
||
{/* <Button variant="primary" leadingIcon={RiRouteFill} className="gap-2"> | ||
Add subscriber | ||
</Button> */} | ||
</div> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.