Browse Source

feat: improve retrieve sms messages query performance and add pagination

pull/52/head
isra el 1 year ago
parent
commit
9a1fff7a5e
  1. 11
      api/src/gateway/gateway.controller.ts
  2. 37
      api/src/gateway/gateway.dto.ts
  3. 33
      api/src/gateway/gateway.service.ts
  4. 3
      api/src/gateway/schemas/sms.schema.ts
  5. 144
      web/app/(app)/dashboard/(components)/received-sms.tsx

11
api/src/gateway/gateway.controller.ts

@ -113,13 +113,20 @@ export class GatewayController {
@ApiOperation({ summary: 'Get received SMS from a device' })
@ApiResponse({ status: 200, type: RetrieveSMSResponseDTO })
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items per page (default: 50, max: 100)' })
@UseGuards(AuthGuard, CanModifyDevice)
// deprecate getReceivedSMS route in favor of get-received-sms
@Get(['/devices/:id/getReceivedSMS', '/devices/:id/get-received-sms'])
async getReceivedSMS(
@Param('id') deviceId: string,
@Request() req,
): Promise<RetrieveSMSResponseDTO> {
const data = await this.gatewayService.getReceivedSMS(deviceId)
return { data }
// Extract page and limit from query params, with defaults and max values
const page = req.query.page ? parseInt(req.query.page, 10) : 1;
const limit = req.query.limit ? Math.min(parseInt(req.query.limit, 10), 100) : 50;
const result = await this.gatewayService.getReceivedSMS(deviceId, page, limit)
return result;
}
}

37
api/src/gateway/gateway.dto.ts

@ -195,6 +195,36 @@ export class RetrieveSMSDTO {
updatedAt: Date
}
export class PaginationMetaDTO {
@ApiProperty({
type: Number,
required: true,
description: 'Current page number',
})
page: number;
@ApiProperty({
type: Number,
required: true,
description: 'Number of items per page',
})
limit: number;
@ApiProperty({
type: Number,
required: true,
description: 'Total number of items',
})
total: number;
@ApiProperty({
type: Number,
required: true,
description: 'Total number of pages',
})
totalPages: number;
}
export class RetrieveSMSResponseDTO {
@ApiProperty({
type: [RetrieveSMSDTO],
@ -202,4 +232,11 @@ export class RetrieveSMSResponseDTO {
description: 'The received SMS data',
})
data: RetrieveSMSDTO[]
@ApiProperty({
type: PaginationMetaDTO,
required: true,
description: 'Pagination metadata',
})
meta?: PaginationMetaDTO
}

33
api/src/gateway/gateway.service.ts

@ -437,7 +437,7 @@ export class GatewayService {
return sms
}
async getReceivedSMS(deviceId: string): Promise<RetrieveSMSDTO[]> {
async getReceivedSMS(deviceId: string, page = 1, limit = 50): Promise<{ data: any[], meta: any }> {
const device = await this.deviceModel.findById(deviceId)
if (!device) {
@ -450,20 +450,47 @@ export class GatewayService {
)
}
// Calculate skip value for pagination
const skip = (page - 1) * limit;
// Get total count for pagination metadata
const total = await this.smsModel.countDocuments({
device: device._id,
type: SMSType.RECEIVED,
});
// @ts-ignore
return await this.smsModel
const data = await this.smsModel
.find(
{
device: device._id,
type: SMSType.RECEIVED,
},
null,
{ sort: { receivedAt: -1 }, limit: 200 },
{
sort: { receivedAt: -1 },
limit: limit,
skip: skip
},
)
.populate({
path: 'device',
select: '_id brand model buildId enabled',
})
.lean() // Use lean() to return plain JavaScript objects instead of Mongoose documents
// Calculate pagination metadata
const totalPages = Math.ceil(total / limit);
return {
meta: {
page,
limit,
total,
totalPages,
},
data,
};
}
async getStatsForUser(user: User) {

3
api/src/gateway/schemas/sms.schema.ts

@ -62,3 +62,6 @@ export class SMS {
}
export const SMSSchema = SchemaFactory.createForClass(SMS)
SMSSchema.index({ device: 1, type: 1, receivedAt: -1 })

144
web/app/(app)/dashboard/(components)/received-sms.tsx

@ -267,6 +267,7 @@ export default function ReceivedSms() {
const handleTabChange = (tab: string) => {
setCurrentTab(tab)
setPage(1)
}
const [currentTab, setCurrentTab] = useState('')
@ -277,19 +278,38 @@ export default function ReceivedSms() {
}
}, [devices])
const [page, setPage] = useState(1)
const [limit, setLimit] = useState(20)
const {
data: receivedSms,
data: receivedSmsResponse,
isLoading: isLoadingReceivedSms,
error: receivedSmsError,
} = useQuery({
queryKey: ['received-sms', currentTab],
queryKey: ['received-sms', currentTab, page, limit],
enabled: !!currentTab,
queryFn: () =>
httpBrowserClient
.get(ApiEndpoints.gateway.getReceivedSMS(currentTab))
.get(
`${ApiEndpoints.gateway.getReceivedSMS(
currentTab
)}?page=${page}&limit=${limit}`
)
.then((res) => res.data),
})
const receivedSms = receivedSmsResponse?.data || []
const pagination = receivedSmsResponse?.meta || {
page: 1,
limit: 20,
total: 0,
totalPages: 1,
}
const handlePageChange = (newPage: number) => {
setPage(newPage)
}
if (isLoadingDevices)
return (
<div className='space-y-4'>
@ -347,15 +367,129 @@ export default function ReceivedSms() {
Error: {receivedSmsError.message}
</div>
)}
{!isLoadingReceivedSms && !receivedSms?.data?.length && (
{!isLoadingReceivedSms && !receivedSms?.length && (
<div className='flex justify-center items-center h-full'>
No messages found
</div>
)}
{receivedSms?.data?.map((sms) => (
{receivedSms?.map((sms) => (
<ReceivedSmsCard key={sms._id} sms={sms} />
))}
{pagination.totalPages > 1 && (
<div className='flex justify-center mt-6 space-x-2'>
<Button
onClick={() => handlePageChange(Math.max(1, page - 1))}
disabled={page === 1}
variant={page === 1 ? 'ghost' : 'default'}
>
Previous
</Button>
<div className='flex flex-wrap items-center gap-2 justify-center sm:justify-start'>
{/* First page */}
{pagination.totalPages > 1 && (
<Button
onClick={() => handlePageChange(1)}
variant={page === 1 ? 'default' : 'ghost'}
size='icon'
className={`h-8 w-8 rounded-full ${
page === 1
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'hover:bg-secondary'
}`}
>
1
</Button>
)}
{/* Ellipsis if needed */}
{page > 4 && pagination.totalPages > 7 && (
<span className='px-1'>...</span>
)}
{/* Middle pages */}
{Array.from(
{ length: Math.min(6, pagination.totalPages - 2) },
(_, i) => {
let pageToShow
if (pagination.totalPages <= 8) {
// If we have 8 or fewer pages, show pages 2 through 7 (or fewer)
pageToShow = i + 2
} else if (page <= 4) {
// Near the start
pageToShow = i + 2
} else if (page >= pagination.totalPages - 3) {
// Near the end
pageToShow = pagination.totalPages - 7 + i
} else {
// Middle - center around current page
pageToShow = page - 2 + i
}
// Ensure page is within bounds and not the first or last page
if (
pageToShow > 1 &&
pageToShow < pagination.totalPages
) {
return (
<Button
key={pageToShow}
onClick={() => handlePageChange(pageToShow)}
variant={page === pageToShow ? 'default' : 'ghost'}
size='icon'
className={`h-8 w-8 rounded-full ${
page === pageToShow
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'hover:bg-secondary'
}`}
>
{pageToShow}
</Button>
)
}
return null
}
)}
{/* Ellipsis if needed */}
{page < pagination.totalPages - 3 &&
pagination.totalPages > 7 && (
<span className='px-1'>...</span>
)}
{/* Last page */}
{pagination.totalPages > 1 && (
<Button
onClick={() => handlePageChange(pagination.totalPages)}
variant={
page === pagination.totalPages ? 'default' : 'ghost'
}
size='icon'
className={`h-8 w-8 rounded-full ${
page === pagination.totalPages
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'hover:bg-secondary'
}`}
>
{pagination.totalPages}
</Button>
)}
</div>
<Button
onClick={() =>
handlePageChange(Math.min(pagination.totalPages, page + 1))
}
disabled={page === pagination.totalPages}
variant={page === pagination.totalPages ? 'ghost' : 'default'}
>
Next
</Button>
</div>
)}
</TabsContent>
))}
</Tabs>

Loading…
Cancel
Save