mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-12 21:44:21 -06:00
Merge branch 'dev' into feature-ai
This commit is contained in:
@@ -3,9 +3,23 @@
|
||||
i18n-title
|
||||
info="Review the log files for the application and for email checking."
|
||||
i18n-info>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
<div class="input-group input-group-sm align-items-center">
|
||||
<div class="input-group input-group-sm me-3">
|
||||
<span class="input-group-text text-muted" i18n>Show</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="100"
|
||||
step="100"
|
||||
[(ngModel)]="limit"
|
||||
(ngModelChange)="onLimitChange($event)"
|
||||
style="width: 100px;">
|
||||
<span class="input-group-text text-muted" i18n>lines</span>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
</div>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
@@ -29,14 +43,19 @@
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="20"
|
||||
class="bg-dark p-3 text-light font-monospace log-container"
|
||||
#logContainer>
|
||||
@if (loading && logFiles.length) {
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
}
|
||||
@for (log of logs; track $index) {
|
||||
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
|
||||
}
|
||||
</div>
|
||||
<p *cdkVirtualFor="let log of logs"
|
||||
class="m-0 p-0"
|
||||
[ngClass]="'log-entry-' + log.level">
|
||||
{{log.message}}
|
||||
</p>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
.log-container {
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 200px);
|
||||
top: 70px;
|
||||
top: 0;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CdkVirtualScrollViewport,
|
||||
ScrollingModule,
|
||||
} from '@angular/cdk/scrolling'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
@@ -38,6 +43,9 @@ describe('LogsComponent', () => {
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
LogsComponent,
|
||||
PageHeaderComponent,
|
||||
CommonModule,
|
||||
CdkVirtualScrollViewport,
|
||||
ScrollingModule,
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
@@ -54,13 +62,12 @@ describe('LogsComponent', () => {
|
||||
fixture = TestBed.createComponent(LogsComponent)
|
||||
component = fixture.componentInstance
|
||||
reloadSpy = jest.spyOn(component, 'reloadLogs')
|
||||
window.HTMLElement.prototype.scroll = function () {} // mock scroll
|
||||
jest.useFakeTimers()
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should display logs with first log initially', () => {
|
||||
expect(logSpy).toHaveBeenCalledWith('paperless')
|
||||
expect(logSpy).toHaveBeenCalledWith('paperless', 5000)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
paperless_logs[0]
|
||||
@@ -71,7 +78,7 @@ describe('LogsComponent', () => {
|
||||
fixture.debugElement
|
||||
.queryAll(By.directive(NgbNavLink))[1]
|
||||
.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(logSpy).toHaveBeenCalledWith('mail')
|
||||
expect(logSpy).toHaveBeenCalledWith('mail', 5000)
|
||||
})
|
||||
|
||||
it('should handle error with no logs', () => {
|
||||
@@ -83,6 +90,10 @@ describe('LogsComponent', () => {
|
||||
})
|
||||
|
||||
it('should auto refresh, allow toggle', () => {
|
||||
jest
|
||||
.spyOn(CdkVirtualScrollViewport.prototype, 'scrollToIndex')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
@@ -90,4 +101,13 @@ describe('LogsComponent', () => {
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should debounce limit changes before reloading logs', () => {
|
||||
const initialCalls = reloadSpy.mock.calls.length
|
||||
component.onLimitChange(6000)
|
||||
jest.advanceTimersByTime(299)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls)
|
||||
jest.advanceTimersByTime(1)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import {
|
||||
CdkVirtualScrollViewport,
|
||||
ScrollingModule,
|
||||
} from '@angular/cdk/scrolling'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
@@ -9,7 +13,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { filter, takeUntil, timer } from 'rxjs'
|
||||
import { Subject, debounceTime, filter, takeUntil, timer } from 'rxjs'
|
||||
import { LogService } from 'src/app/services/rest/log.service'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
@@ -21,8 +25,11 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
imports: [
|
||||
PageHeaderComponent,
|
||||
NgbNavModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CdkVirtualScrollViewport,
|
||||
ScrollingModule,
|
||||
],
|
||||
})
|
||||
export class LogsComponent
|
||||
@@ -32,7 +39,7 @@ export class LogsComponent
|
||||
private logService = inject(LogService)
|
||||
private changedetectorRef = inject(ChangeDetectorRef)
|
||||
|
||||
public logs: string[] = []
|
||||
public logs: Array<{ message: string; level: number }> = []
|
||||
|
||||
public logFiles: string[] = []
|
||||
|
||||
@@ -40,9 +47,17 @@ export class LogsComponent
|
||||
|
||||
public autoRefreshEnabled: boolean = true
|
||||
|
||||
@ViewChild('logContainer') logContainer: ElementRef
|
||||
public limit: number = 5000
|
||||
|
||||
private readonly limitChange$ = new Subject<number>()
|
||||
|
||||
@ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
|
||||
|
||||
ngOnInit(): void {
|
||||
this.limitChange$
|
||||
.pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => this.reloadLogs())
|
||||
|
||||
this.logService
|
||||
.list()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
@@ -68,16 +83,33 @@ export class LogsComponent
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
onLimitChange(limit: number): void {
|
||||
this.limitChange$.next(limit)
|
||||
}
|
||||
|
||||
reloadLogs() {
|
||||
this.loading = true
|
||||
this.logService
|
||||
.get(this.activeLog)
|
||||
.get(this.activeLog, this.limit)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.logs = result
|
||||
this.loading = false
|
||||
this.scrollToBottom()
|
||||
const parsed = this.parseLogsWithLevel(result)
|
||||
const hasChanges =
|
||||
parsed.length !== this.logs.length ||
|
||||
parsed.some((log, idx) => {
|
||||
const current = this.logs[idx]
|
||||
return (
|
||||
!current ||
|
||||
current.message !== log.message ||
|
||||
current.level !== log.level
|
||||
)
|
||||
})
|
||||
if (hasChanges) {
|
||||
this.logs = parsed
|
||||
this.scrollToBottom()
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.logs = []
|
||||
@@ -100,12 +132,19 @@ export class LogsComponent
|
||||
}
|
||||
}
|
||||
|
||||
private parseLogsWithLevel(
|
||||
logs: string[]
|
||||
): Array<{ message: string; level: number }> {
|
||||
return logs.map((log) => ({
|
||||
message: log,
|
||||
level: this.getLogLevel(log),
|
||||
}))
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
this.changedetectorRef.detectChanges()
|
||||
this.logContainer?.nativeElement.scroll({
|
||||
top: this.logContainer.nativeElement.scrollHeight,
|
||||
left: 0,
|
||||
behavior: 'auto',
|
||||
})
|
||||
if (this.logContainer) {
|
||||
this.logContainer.scrollToIndex(this.logs.length - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,13 +71,15 @@
|
||||
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
|
||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
||||
[ngbCollapse]="isMenuCollapsed">
|
||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||
@if (slimSidebarEnabled) {
|
||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
||||
} @else {
|
||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
||||
}
|
||||
</button>
|
||||
@if (canSaveSettings) {
|
||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||
@if (slimSidebarEnabled) {
|
||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
||||
} @else {
|
||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item app-link">
|
||||
|
||||
@@ -154,6 +154,19 @@ export class AppFrameComponent
|
||||
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||
}
|
||||
|
||||
get canSaveSettings(): boolean {
|
||||
return (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.Change,
|
||||
PermissionType.UISettings
|
||||
) &&
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.Add,
|
||||
PermissionType.UISettings
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
get slimSidebarEnabled(): boolean {
|
||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||
}
|
||||
|
||||
@@ -1544,6 +1544,8 @@ describe('DocumentDetailComponent', () => {
|
||||
mockContentWindow.onafterprint(new Event('afterprint'))
|
||||
}
|
||||
|
||||
tick(500)
|
||||
|
||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||
|
||||
@@ -1567,65 +1569,97 @@ describe('DocumentDetailComponent', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should show error toast if printing throws inside iframe', fakeAsync(() => {
|
||||
initNormally()
|
||||
const iframePrintErrorCases: Array<{
|
||||
description: string
|
||||
thrownError: Error
|
||||
expectToast: boolean
|
||||
}> = [
|
||||
{
|
||||
description: 'should show error toast if printing throws inside iframe',
|
||||
thrownError: new Error('focus failed'),
|
||||
expectToast: true,
|
||||
},
|
||||
{
|
||||
description:
|
||||
'should suppress toast if cross-origin afterprint error occurs',
|
||||
thrownError: new DOMException(
|
||||
'Accessing onafterprint triggered a cross-origin violation',
|
||||
'SecurityError'
|
||||
),
|
||||
expectToast: false,
|
||||
},
|
||||
]
|
||||
|
||||
const appendChildSpy = jest
|
||||
.spyOn(document.body, 'appendChild')
|
||||
.mockImplementation((node: Node) => node)
|
||||
const removeChildSpy = jest
|
||||
.spyOn(document.body, 'removeChild')
|
||||
.mockImplementation((node: Node) => node)
|
||||
const createObjectURLSpy = jest
|
||||
.spyOn(URL, 'createObjectURL')
|
||||
.mockReturnValue('blob:mock-url')
|
||||
const revokeObjectURLSpy = jest
|
||||
.spyOn(URL, 'revokeObjectURL')
|
||||
.mockImplementation(() => {})
|
||||
iframePrintErrorCases.forEach(({ description, thrownError, expectToast }) => {
|
||||
it(
|
||||
description,
|
||||
fakeAsync(() => {
|
||||
initNormally()
|
||||
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
const appendChildSpy = jest
|
||||
.spyOn(document.body, 'appendChild')
|
||||
.mockImplementation((node: Node) => node)
|
||||
const removeChildSpy = jest
|
||||
.spyOn(document.body, 'removeChild')
|
||||
.mockImplementation((node: Node) => node)
|
||||
const createObjectURLSpy = jest
|
||||
.spyOn(URL, 'createObjectURL')
|
||||
.mockReturnValue('blob:mock-url')
|
||||
const revokeObjectURLSpy = jest
|
||||
.spyOn(URL, 'revokeObjectURL')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const mockContentWindow = {
|
||||
focus: jest.fn().mockImplementation(() => {
|
||||
throw new Error('focus failed')
|
||||
}),
|
||||
print: jest.fn(),
|
||||
onafterprint: null,
|
||||
}
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
|
||||
const mockIframe: any = {
|
||||
style: {},
|
||||
src: '',
|
||||
onload: null,
|
||||
contentWindow: mockContentWindow,
|
||||
}
|
||||
const mockContentWindow = {
|
||||
focus: jest.fn().mockImplementation(() => {
|
||||
throw thrownError
|
||||
}),
|
||||
print: jest.fn(),
|
||||
onafterprint: null,
|
||||
}
|
||||
|
||||
const createElementSpy = jest
|
||||
.spyOn(document, 'createElement')
|
||||
.mockReturnValue(mockIframe as any)
|
||||
const mockIframe: any = {
|
||||
style: {},
|
||||
src: '',
|
||||
onload: null,
|
||||
contentWindow: mockContentWindow,
|
||||
}
|
||||
|
||||
const blob = new Blob(['test'], { type: 'application/pdf' })
|
||||
component.printDocument()
|
||||
const createElementSpy = jest
|
||||
.spyOn(document, 'createElement')
|
||||
.mockReturnValue(mockIframe as any)
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/${doc.id}/download/`
|
||||
const blob = new Blob(['test'], { type: 'application/pdf' })
|
||||
component.printDocument()
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/${doc.id}/download/`
|
||||
)
|
||||
req.flush(blob)
|
||||
|
||||
tick()
|
||||
|
||||
if (mockIframe.onload) {
|
||||
mockIframe.onload(new Event('load'))
|
||||
}
|
||||
|
||||
tick(200)
|
||||
|
||||
if (expectToast) {
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
} else {
|
||||
expect(toastSpy).not.toHaveBeenCalled()
|
||||
}
|
||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||
|
||||
createElementSpy.mockRestore()
|
||||
appendChildSpy.mockRestore()
|
||||
removeChildSpy.mockRestore()
|
||||
createObjectURLSpy.mockRestore()
|
||||
revokeObjectURLSpy.mockRestore()
|
||||
})
|
||||
)
|
||||
req.flush(blob)
|
||||
|
||||
tick()
|
||||
|
||||
if (mockIframe.onload) {
|
||||
mockIframe.onload(new Event('load'))
|
||||
}
|
||||
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||
|
||||
createElementSpy.mockRestore()
|
||||
appendChildSpy.mockRestore()
|
||||
removeChildSpy.mockRestore()
|
||||
createObjectURLSpy.mockRestore()
|
||||
revokeObjectURLSpy.mockRestore()
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'
|
||||
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
@@ -1521,9 +1521,18 @@ export class DocumentDetailComponent
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
} catch (err) {
|
||||
this.toastService.showError($localize`Print failed.`, err)
|
||||
document.body.removeChild(iframe)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
// FF throws cross-origin error on onafterprint
|
||||
const isCrossOriginAfterPrintError =
|
||||
err instanceof DOMException &&
|
||||
err.message.includes('onafterprint')
|
||||
if (!isCrossOriginAfterPrintError) {
|
||||
this.toastService.showError($localize`Print failed.`, err)
|
||||
}
|
||||
timer(100).subscribe(() => {
|
||||
// delay to avoid FF print failure
|
||||
document.body.removeChild(iframe)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ng-template #errorPopover>
|
||||
<pre class="small text-light">
|
||||
<pre class="small">
|
||||
{{ mail.error }}
|
||||
</pre>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
::ng-deep .popover {
|
||||
max-width: 350px;
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
|
||||
@@ -73,9 +73,14 @@ describe('TagListComponent', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should filter out child tags if name filter is empty, otherwise show all', () => {
|
||||
it('should omit matching children from top level when their parent is present', () => {
|
||||
const tags = [
|
||||
{ id: 1, name: 'Tag1', parent: null },
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
parent: null,
|
||||
children: [{ id: 2, name: 'Tag2', parent: 1 }],
|
||||
},
|
||||
{ id: 2, name: 'Tag2', parent: 1 },
|
||||
{ id: 3, name: 'Tag3', parent: null },
|
||||
]
|
||||
@@ -86,7 +91,13 @@ describe('TagListComponent', () => {
|
||||
|
||||
component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter
|
||||
const filteredWithName = component.filterData(tags as any)
|
||||
expect(filteredWithName.length).toBe(3)
|
||||
expect(filteredWithName.length).toBe(2)
|
||||
expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined()
|
||||
expect(
|
||||
filteredWithName
|
||||
.find((t) => t.id === 1)
|
||||
?.children?.some((c) => c.id === 2)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should request only parent tags when no name filter is applied', () => {
|
||||
|
||||
@@ -69,9 +69,13 @@ export class TagListComponent extends ManagementListComponent<Tag> {
|
||||
}
|
||||
|
||||
filterData(data: Tag[]) {
|
||||
return this.nameFilter?.length
|
||||
? [...data]
|
||||
: data.filter((tag) => !tag.parent)
|
||||
if (!this.nameFilter?.length) {
|
||||
return data.filter((tag) => !tag.parent)
|
||||
}
|
||||
|
||||
// When filtering by name, exclude children if their parent is also present
|
||||
const availableIds = new Set(data.map((tag) => tag.id))
|
||||
return data.filter((tag) => !tag.parent || !availableIds.has(tag.parent))
|
||||
}
|
||||
|
||||
protected override getSelectableIDs(tags: Tag[]): number[] {
|
||||
|
||||
@@ -49,4 +49,14 @@ describe('LogService', () => {
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
|
||||
it('should pass limit param on logs get when provided', () => {
|
||||
const id: string = 'mail'
|
||||
const limit: number = 100
|
||||
subscription = service.get(id, limit).subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/${id}/?limit=${limit}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
@@ -13,7 +13,13 @@ export class LogService {
|
||||
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/`)
|
||||
}
|
||||
|
||||
get(id: string): Observable<string[]> {
|
||||
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`)
|
||||
get(id: string, limit?: number): Observable<string[]> {
|
||||
let params = new HttpParams()
|
||||
if (limit !== undefined) {
|
||||
params = params.set('limit', limit.toString())
|
||||
}
|
||||
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`, {
|
||||
params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user