Performance: reduce tasks badge load times (#14552)

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
Jan Kleineandshamoon authored and GitHub committed 2026-10-26 14:23:33 +00:00
1 parent 5f17b1a310
commit 7d8147c21d
5 files changed
+52 -137

No files matched your search

@@ -19,7 +19,6 @@ import {
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { Results } from 'src/app/data/results'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -153,11 +152,6 @@ const tasks: PaperlessTask[] = [
},
]
const paginatedTasks: Results<PaperlessTask> = {
count: tasks.length,
results: tasks,
}
const sectionCountResponse = {
all: 7,
needs_attention: 2,
@@ -204,7 +198,7 @@ describe('TasksComponent', () => {
}).compileComponents()
tasksService = TestBed.inject(TasksService)
reloadSpy = jest.spyOn(tasksService, 'reload')
reloadSpy = jest.spyOn(tasksService, 'reload').mockImplementation(() => {})
httpTestingController = TestBed.inject(HttpTestingController)
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
@@ -214,15 +208,6 @@ describe('TasksComponent', () => {
jest.useFakeTimers()
fixture.detectChanges()
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
.flush(paginatedTasks)
httpTestingController
.expectOne(
(req) =>
@@ -344,11 +344,11 @@
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="list-task"></i-bs><span class="nav-link-label"><ng-container i18n>Tasks</ng-container></span>
@if (tasksService.needsAttentionTasks.length > 0 && !slimSidebarEnabled) {
<span class="badge bg-danger ms-2 d-inline flex-shrink-0">{{tasksService.needsAttentionTasks.length}}</span>
@if (tasksService.needsAttentionTasksCount > 0 && !slimSidebarEnabled) {
<span class="badge bg-danger ms-2 d-inline flex-shrink-0">{{tasksService.needsAttentionTasksCount}}</span>
}
@if (tasksService.needsAttentionTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.needsAttentionTasks.length}}</span>
@if (tasksService.needsAttentionTasksCount > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.needsAttentionTasksCount}}</span>
}
</a>
</li>
@@ -588,8 +588,8 @@ describe('AppFrameComponent', () => {
it('should show tasks badge for needs-attention tasks', () => {
jest
.spyOn(tasksService, 'needsAttentionTasks', 'get')
.mockReturnValue([{} as any, {} as any])
.spyOn(tasksService, 'needsAttentionTasksCount', 'get')
.mockReturnValue(2)
fixture.detectChanges()
+22 -82
View File
@@ -9,11 +9,7 @@ import {
} from '@angular/common/http/testing'
import { TestBed } from '@angular/core/testing'
import { environment } from 'src/environments/environment'
import {
PaperlessTaskStatus,
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from '../data/paperless-task'
import { PaperlessTaskType } from '../data/paperless-task'
import { TasksService } from './tasks.service'
describe('TasksService', () => {
@@ -38,23 +34,23 @@ describe('TasksService', () => {
httpTestingController.verify()
})
it('calls tasks api endpoint on reload', () => {
it('calls tasks status counts api endpoint on reload', () => {
tasksService.reload()
const req = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
req.url === `${environment.apiBaseUrl}tasks/status_counts/` &&
req.params.get('acknowledged') === 'false'
)
expect(req.request.method).toEqual('GET')
req.flush({ count: 0, results: [] })
req.flush({ all: 5, needs_attention: 3, in_progress: 1, completed: 1 })
expect(tasksService.needsAttentionTasksCount).toEqual(3)
})
it('cancels an in-progress reload when reloading again', () => {
tasksService.reload()
const staleReload = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
req.url === `${environment.apiBaseUrl}tasks/status_counts/`
)
tasksService.reload()
@@ -62,9 +58,9 @@ describe('TasksService', () => {
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
req.url === `${environment.apiBaseUrl}tasks/status_counts/`
)
.flush({ count: 0, results: [] })
.flush({ all: 0, needs_attention: 0, in_progress: 0, completed: 0 })
})
it('continues reloading after a reload request fails', () => {
@@ -72,7 +68,7 @@ describe('TasksService', () => {
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
req.url === `${environment.apiBaseUrl}tasks/status_counts/`
)
.flush('error', { status: 500, statusText: 'error' })
@@ -82,16 +78,16 @@ describe('TasksService', () => {
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
req.url === `${environment.apiBaseUrl}tasks/status_counts/`
)
.flush({ count: 0, results: [] })
.flush({ all: 0, needs_attention: 0, in_progress: 0, completed: 0 })
})
it('reloads after dismissing a task while a reload is already in progress', () => {
tasksService.reload()
const staleReload = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.url === `${environment.apiBaseUrl}tasks/status_counts/` &&
req.params.get('acknowledged') === 'false'
)
@@ -104,12 +100,12 @@ describe('TasksService', () => {
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.url === `${environment.apiBaseUrl}tasks/status_counts/` &&
req.params.get('acknowledged') === 'false'
)
.flush({ count: 0, results: [] })
.flush({ all: 50, needs_attention: 42, in_progress: 3, completed: 5 })
expect(tasksService.needsAttentionTasks).toHaveLength(0)
expect(tasksService.needsAttentionTasksCount).toEqual(42)
})
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
@@ -126,11 +122,10 @@ describe('TasksService', () => {
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
req.url === `${environment.apiBaseUrl}tasks/status_counts/` &&
req.params.get('acknowledged') === 'false'
)
.flush({ count: 0, results: [] })
.flush({ all: 0, needs_attention: 0, in_progress: 0, completed: 0 })
})
it('calls acknowledge_tasks api endpoint on dismiss all and reloads', () => {
@@ -147,65 +142,10 @@ describe('TasksService', () => {
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
req.url === `${environment.apiBaseUrl}tasks/status_counts/` &&
req.params.get('acknowledged') === 'false'
)
.flush({ count: 0, results: [] })
})
it('includes revoked tasks in needs attention', () => {
const mockTasks = [
{
task_type: PaperlessTaskType.SanityCheck,
trigger_source: PaperlessTaskTriggerSource.System,
status: PaperlessTaskStatus.Failure,
acknowledged: false,
task_id: '1235',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.MailFetch,
trigger_source: PaperlessTaskTriggerSource.Scheduled,
status: PaperlessTaskStatus.Revoked,
acknowledged: false,
task_id: '1236',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.EmptyTrash,
trigger_source: PaperlessTaskTriggerSource.Manual,
status: PaperlessTaskStatus.Success,
acknowledged: false,
task_id: '1238',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
]
tasksService.reload()
const req = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
req.flush({ count: mockTasks.length, results: mockTasks })
expect(tasksService.needsAttentionTasks).toHaveLength(2)
expect(tasksService.needsAttentionTasks.map((task) => task.status)).toEqual(
expect.arrayContaining([
PaperlessTaskStatus.Failure,
PaperlessTaskStatus.Revoked,
])
)
.flush({ all: 0, needs_attention: 0, in_progress: 0, completed: 0 })
})
it('supports running tasks', () => {
+23 -33
View File
@@ -5,14 +5,12 @@ import {
catchError,
finalize,
first,
map,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators'
import {
PaperlessTask,
PaperlessTaskStatus,
PaperlessTaskStatusCounts,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
@@ -27,50 +25,42 @@ export class TasksService {
private baseUrl: string = environment.apiBaseUrl
private endpoint: string = 'tasks'
private readonly defaultReloadPageSize = 1000
public loading: boolean = false
private readonly tasks = signal<PaperlessTask[]>([])
private readonly tasksStatusCounts = signal<PaperlessTaskStatusCounts>({
all: 0,
needs_attention: 0,
in_progress: 0,
completed: 0,
})
private readonly reloadNotifier = new Subject<void>()
private unsubscribeNotifer: Subject<any> = new Subject()
constructor() {
this.reloadNotifier
.pipe(
switchMap(() => {
this.loading = true
return this.http
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
acknowledged: 'false',
page_size: this.defaultReloadPageSize,
},
})
.pipe(
map((response) => response.results),
takeUntil(this.unsubscribeNotifer),
catchError(() => EMPTY),
finalize(() => {
this.loading = false
})
)
})
)
.subscribe((tasks) => {
this.tasks.set(tasks)
.pipe(switchMap(() => this.fetchUnacknowledgedTasksCounts()))
.subscribe((taskStatusCounts) => {
this.tasksStatusCounts.set(taskStatusCounts)
})
}
public get needsAttentionTasks(): PaperlessTask[] {
return this.tasks().filter((t) =>
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
t.status
)
private fetchUnacknowledgedTasksCounts(): Observable<PaperlessTaskStatusCounts> {
this.loading = true
return this.statusCounts({ acknowledged: false }).pipe(
takeUntil(this.unsubscribeNotifer),
catchError(() => EMPTY),
finalize(() => {
this.loading = false
})
)
}
public get needsAttentionTasksCount(): number {
return this.tasksStatusCounts().needs_attention
}
public reload() {
this.reloadNotifier.next()
}
@@ -105,7 +95,7 @@ export class TasksService {
public dismissTasks(task_ids: Set<number>): Observable<any> {
return this.http
.post(`${this.baseUrl}tasks/acknowledge/`, {
.post(`${this.baseUrl}${this.endpoint}/acknowledge/`, {
tasks: [...task_ids],
})
.pipe(
@@ -119,7 +109,7 @@ export class TasksService {
public dismissAllTasks(): Observable<any> {
return this.http
.post(`${this.baseUrl}tasks/acknowledge/`, {
.post(`${this.baseUrl}${this.endpoint}/acknowledge/`, {
all: true,
})
.pipe(