mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 17:25:11 +03:00
feat: paginate integrity report results
This commit is contained in:
@@ -13,25 +13,59 @@ part of openapi.api;
|
||||
class IntegrityGetReportDto {
|
||||
/// Returns a new [IntegrityGetReportDto] instance.
|
||||
IntegrityGetReportDto({
|
||||
this.page,
|
||||
this.size,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
/// Minimum value: 1
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? page;
|
||||
|
||||
/// Minimum value: 1
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? size;
|
||||
|
||||
IntegrityReportType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is IntegrityGetReportDto &&
|
||||
other.page == page &&
|
||||
other.size == size &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(page == null ? 0 : page!.hashCode) +
|
||||
(size == null ? 0 : size!.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'IntegrityGetReportDto[type=$type]';
|
||||
String toString() => 'IntegrityGetReportDto[page=$page, size=$size, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.page != null) {
|
||||
json[r'page'] = this.page;
|
||||
} else {
|
||||
// json[r'page'] = null;
|
||||
}
|
||||
if (this.size != null) {
|
||||
json[r'size'] = this.size;
|
||||
} else {
|
||||
// json[r'size'] = null;
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
@@ -45,6 +79,8 @@ class IntegrityGetReportDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return IntegrityGetReportDto(
|
||||
page: num.parse('${json[r'page']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
type: IntegrityReportType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,25 +13,31 @@ part of openapi.api;
|
||||
class IntegrityReportResponseDto {
|
||||
/// Returns a new [IntegrityReportResponseDto] instance.
|
||||
IntegrityReportResponseDto({
|
||||
required this.hasNextPage,
|
||||
this.items = const [],
|
||||
});
|
||||
|
||||
bool hasNextPage;
|
||||
|
||||
List<IntegrityReportDto> items;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is IntegrityReportResponseDto &&
|
||||
other.hasNextPage == hasNextPage &&
|
||||
_deepEquality.equals(other.items, items);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(hasNextPage.hashCode) +
|
||||
(items.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'IntegrityReportResponseDto[items=$items]';
|
||||
String toString() => 'IntegrityReportResponseDto[hasNextPage=$hasNextPage, items=$items]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'hasNextPage'] = this.hasNextPage;
|
||||
json[r'items'] = this.items;
|
||||
return json;
|
||||
}
|
||||
@@ -45,6 +51,7 @@ class IntegrityReportResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return IntegrityReportResponseDto(
|
||||
hasNextPage: mapValueOfType<bool>(json, r'hasNextPage')!,
|
||||
items: IntegrityReportDto.listFromJson(json[r'items']),
|
||||
);
|
||||
}
|
||||
@@ -93,6 +100,7 @@ class IntegrityReportResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'hasNextPage',
|
||||
'items',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16864,6 +16864,14 @@
|
||||
},
|
||||
"IntegrityGetReportDto": {
|
||||
"properties": {
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -16902,6 +16910,9 @@
|
||||
},
|
||||
"IntegrityReportResponseDto": {
|
||||
"properties": {
|
||||
"hasNextPage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/IntegrityReportDto"
|
||||
@@ -16910,6 +16921,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hasNextPage",
|
||||
"items"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
@@ -41,6 +41,8 @@ export type ActivityStatisticsResponseDto = {
|
||||
likes: number;
|
||||
};
|
||||
export type IntegrityGetReportDto = {
|
||||
page?: number;
|
||||
size?: number;
|
||||
"type": IntegrityReportType;
|
||||
};
|
||||
export type IntegrityReportDto = {
|
||||
@@ -49,6 +51,7 @@ export type IntegrityReportDto = {
|
||||
"type": IntegrityReportType;
|
||||
};
|
||||
export type IntegrityReportResponseDto = {
|
||||
hasNextPage: boolean;
|
||||
items: IntegrityReportDto[];
|
||||
};
|
||||
export type IntegrityReportSummaryResponseDto = {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class IntegrityController {
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReport(@Body() dto: IntegrityGetReportDto): Promise<IntegrityReportResponseDto> {
|
||||
return this.service.getIntegrityReport(dto.type);
|
||||
return this.service.getIntegrityReport(dto);
|
||||
}
|
||||
|
||||
@Delete('report/:id')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Min } from 'class-validator';
|
||||
import { IntegrityReportType } from 'src/enum';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
@@ -15,12 +17,17 @@ export class IntegrityGetReportDto {
|
||||
@ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' })
|
||||
type!: IntegrityReportType;
|
||||
|
||||
// todo: paginate
|
||||
// @IsInt()
|
||||
// @Min(1)
|
||||
// @Type(() => Number)
|
||||
// @Optional()
|
||||
// page?: number;
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
page?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export class IntegrityDeleteReportDto {
|
||||
@@ -37,4 +44,5 @@ class IntegrityReportDto {
|
||||
|
||||
export class IntegrityReportResponseDto {
|
||||
items!: IntegrityReportDto[];
|
||||
hasNextPage!: boolean;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { IntegrityReportType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
|
||||
import { paginationHelper } from 'src/utils/pagination';
|
||||
|
||||
export interface ReportPaginationOptions {
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IntegrityRepository {
|
||||
@@ -58,14 +64,18 @@ export class IntegrityRepository {
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getIntegrityReports(type: IntegrityReportType) {
|
||||
return this.db
|
||||
@GenerateSql({ params: [{ page: 1, size: 100 }, DummyValue.STRING] })
|
||||
async getIntegrityReports(pagination: ReportPaginationOptions, type: IntegrityReportType) {
|
||||
const items = await this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select(['id', 'type', 'path', 'assetId', 'fileAssetId'])
|
||||
.where('type', '=', type)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(pagination.size + 1)
|
||||
.offset((pagination.page - 1) * pagination.size)
|
||||
.execute();
|
||||
|
||||
return paginationHelper(items, pagination.size);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
|
||||
@@ -24,7 +24,7 @@ describe(IntegrityService.name, () => {
|
||||
|
||||
describe('getIntegrityReport', () => {
|
||||
it('gets report', async () => {
|
||||
await expect(sut.getIntegrityReport(IntegrityReportType.ChecksumFail)).resolves.toEqual(
|
||||
await expect(sut.getIntegrityReport({ type: IntegrityReportType.ChecksumFail })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
items: undefined,
|
||||
}),
|
||||
|
||||
@@ -9,7 +9,11 @@ import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { IntegrityReportResponseDto, IntegrityReportSummaryResponseDto } from 'src/dtos/integrity.dto';
|
||||
import {
|
||||
IntegrityGetReportDto,
|
||||
IntegrityReportResponseDto,
|
||||
IntegrityReportSummaryResponseDto,
|
||||
} from 'src/dtos/integrity.dto';
|
||||
import {
|
||||
AssetStatus,
|
||||
CacheControl,
|
||||
@@ -154,10 +158,8 @@ export class IntegrityService extends BaseService {
|
||||
return this.integrityRepository.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
async getIntegrityReport(type: IntegrityReportType): Promise<IntegrityReportResponseDto> {
|
||||
return {
|
||||
items: await this.integrityRepository.getIntegrityReports(type),
|
||||
};
|
||||
async getIntegrityReport(dto: IntegrityGetReportDto): Promise<IntegrityReportResponseDto> {
|
||||
return this.integrityRepository.getIntegrityReports({ page: dto.page || 1, size: dto.size || 100 }, dto.type);
|
||||
}
|
||||
|
||||
getIntegrityReportCsv(type: IntegrityReportType): Readable {
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createJob, deleteIntegrityReport, getBaseUrl, IntegrityReportType, ManualJobName } from '@immich/sdk';
|
||||
import {
|
||||
createJob,
|
||||
deleteIntegrityReport,
|
||||
getBaseUrl,
|
||||
getIntegrityReport,
|
||||
IntegrityReportType,
|
||||
ManualJobName,
|
||||
} from '@immich/sdk';
|
||||
import {
|
||||
HStack,
|
||||
IconButton,
|
||||
menuManager,
|
||||
modalManager,
|
||||
@@ -11,7 +19,14 @@
|
||||
type ContextMenuBaseProps,
|
||||
type MenuItems,
|
||||
} from '@immich/ui';
|
||||
import { mdiDotsVertical, mdiDownload, mdiTrashCanOutline } from '@mdi/js';
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
mdiPageFirst,
|
||||
mdiTrashCanOutline,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { PageData } from './$types';
|
||||
@@ -23,7 +38,19 @@
|
||||
let { data }: Props = $props();
|
||||
|
||||
let deleting = new SvelteSet();
|
||||
let integrityReport = $state(data.integrityReport.items);
|
||||
let page = $state(1);
|
||||
let integrityReport = $state(data.integrityReport);
|
||||
|
||||
async function loadPage(target: number) {
|
||||
integrityReport = await getIntegrityReport({
|
||||
integrityGetReportDto: {
|
||||
type: data.type,
|
||||
page: target,
|
||||
},
|
||||
});
|
||||
|
||||
page = target;
|
||||
}
|
||||
|
||||
async function removeAll() {
|
||||
const confirm = await modalManager.showDialog({
|
||||
@@ -68,7 +95,7 @@
|
||||
await deleteIntegrityReport({
|
||||
id,
|
||||
});
|
||||
integrityReport = integrityReport.filter((report) => report.id !== id);
|
||||
integrityReport.items = integrityReport.items.filter((report) => report.id !== id);
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to delete file!');
|
||||
} finally {
|
||||
@@ -147,7 +174,7 @@
|
||||
<tbody
|
||||
class="block max-h-80 w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
{#each integrityReport as { id, path } (id)}
|
||||
{#each integrityReport.items as { id, path } (id)}
|
||||
<tr
|
||||
class={`flex py-1 w-full place-items-center even:bg-subtle/20 odd:bg-subtle/80 ${deleting.has(id) || deleting.has('all') ? 'text-gray-500' : ''}`}
|
||||
>
|
||||
@@ -165,6 +192,31 @@
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<HStack class="mt-4 items-center justify-end">
|
||||
<IconButton
|
||||
disabled={page === 1}
|
||||
color="primary"
|
||||
icon={mdiPageFirst}
|
||||
aria-label="first page"
|
||||
onclick={() => loadPage(1)}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={page === 1}
|
||||
color="primary"
|
||||
icon={mdiChevronLeft}
|
||||
aria-label="previous page"
|
||||
onclick={() => loadPage(page - 1)}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={!integrityReport.hasNextPage}
|
||||
color="primary"
|
||||
icon={mdiChevronRight}
|
||||
aria-label="next page"
|
||||
onclick={() => loadPage(page + 1)}
|
||||
/>
|
||||
</HStack>
|
||||
</tfoot>
|
||||
</table>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user