mirror of
https://github.com/immich-app/immich.git
synced 2024-12-26 10:50:29 +02:00
feat(web) add asset count stats on admin page (#843)
This commit is contained in:
parent
2c189d5c78
commit
a6eea4d096
@ -48,6 +48,7 @@ doc/SearchAssetDto.md
|
|||||||
doc/ServerInfoApi.md
|
doc/ServerInfoApi.md
|
||||||
doc/ServerInfoResponseDto.md
|
doc/ServerInfoResponseDto.md
|
||||||
doc/ServerPingResponse.md
|
doc/ServerPingResponse.md
|
||||||
|
doc/ServerStatsResponseDto.md
|
||||||
doc/ServerVersionReponseDto.md
|
doc/ServerVersionReponseDto.md
|
||||||
doc/SignUpDto.md
|
doc/SignUpDto.md
|
||||||
doc/SmartInfoResponseDto.md
|
doc/SmartInfoResponseDto.md
|
||||||
@ -56,6 +57,7 @@ doc/TimeGroupEnum.md
|
|||||||
doc/UpdateAlbumDto.md
|
doc/UpdateAlbumDto.md
|
||||||
doc/UpdateDeviceInfoDto.md
|
doc/UpdateDeviceInfoDto.md
|
||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
|
doc/UsageByUserDto.md
|
||||||
doc/UserApi.md
|
doc/UserApi.md
|
||||||
doc/UserCountResponseDto.md
|
doc/UserCountResponseDto.md
|
||||||
doc/UserResponseDto.md
|
doc/UserResponseDto.md
|
||||||
@ -117,6 +119,7 @@ lib/model/remove_assets_dto.dart
|
|||||||
lib/model/search_asset_dto.dart
|
lib/model/search_asset_dto.dart
|
||||||
lib/model/server_info_response_dto.dart
|
lib/model/server_info_response_dto.dart
|
||||||
lib/model/server_ping_response.dart
|
lib/model/server_ping_response.dart
|
||||||
|
lib/model/server_stats_response_dto.dart
|
||||||
lib/model/server_version_reponse_dto.dart
|
lib/model/server_version_reponse_dto.dart
|
||||||
lib/model/sign_up_dto.dart
|
lib/model/sign_up_dto.dart
|
||||||
lib/model/smart_info_response_dto.dart
|
lib/model/smart_info_response_dto.dart
|
||||||
@ -125,6 +128,7 @@ lib/model/time_group_enum.dart
|
|||||||
lib/model/update_album_dto.dart
|
lib/model/update_album_dto.dart
|
||||||
lib/model/update_device_info_dto.dart
|
lib/model/update_device_info_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
|
lib/model/usage_by_user_dto.dart
|
||||||
lib/model/user_count_response_dto.dart
|
lib/model/user_count_response_dto.dart
|
||||||
lib/model/user_response_dto.dart
|
lib/model/user_response_dto.dart
|
||||||
lib/model/validate_access_token_response_dto.dart
|
lib/model/validate_access_token_response_dto.dart
|
||||||
|
Binary file not shown.
BIN
mobile/openapi/doc/AssetCountResponseDto.md
Normal file
BIN
mobile/openapi/doc/AssetCountResponseDto.md
Normal file
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/doc/ServerStatsResponseDto.md
Normal file
BIN
mobile/openapi/doc/ServerStatsResponseDto.md
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/UsageByUserDto.md
Normal file
BIN
mobile/openapi/doc/UsageByUserDto.md
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_count_response_dto.dart
Normal file
BIN
mobile/openapi/lib/model/asset_count_response_dto.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/server_stats_response_dto.dart
Normal file
BIN
mobile/openapi/lib/model/server_stats_response_dto.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/usage_by_user_dto.dart
Normal file
BIN
mobile/openapi/lib/model/usage_by_user_dto.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_count_response_dto_test.dart
Normal file
BIN
mobile/openapi/test/asset_count_response_dto_test.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/test/server_stats_response_dto_test.dart
Normal file
BIN
mobile/openapi/test/server_stats_response_dto_test.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/test/usage_by_user_dto_test.dart
Normal file
BIN
mobile/openapi/test/usage_by_user_dto_test.dart
Normal file
Binary file not shown.
@ -182,6 +182,7 @@ export class AssetController {
|
|||||||
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||||
return this.assetService.getAssetCountByUserId(authUser);
|
return this.assetService.getAssetCountByUserId(authUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all AssetEntity belong to the user
|
* Get all AssetEntity belong to the user
|
||||||
*/
|
*/
|
||||||
|
@ -54,7 +54,7 @@ export class AuthService {
|
|||||||
const validatedUser = await this.validateUser(loginCredential);
|
const validatedUser = await this.validateUser(loginCredential);
|
||||||
|
|
||||||
if (!validatedUser) {
|
if (!validatedUser) {
|
||||||
Logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`)
|
Logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`);
|
||||||
throw new BadRequestException('Incorrect email or password');
|
throw new BadRequestException('Incorrect email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ApiResponseProperty } from '@nestjs/swagger';
|
import { ApiResponseProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class LogoutResponseDto {
|
export class LogoutResponseDto {
|
||||||
constructor (successful: boolean) {
|
constructor(successful: boolean) {
|
||||||
this.successful = successful;
|
this.successful = successful;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiResponseProperty()
|
@ApiResponseProperty()
|
||||||
successful!: boolean;
|
successful!: boolean;
|
||||||
};
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { UsageByUserDto } from './usage-by-user-response.dto';
|
||||||
|
|
||||||
|
export class ServerStatsResponseDto {
|
||||||
|
constructor() {
|
||||||
|
this.photos = 0;
|
||||||
|
this.videos = 0;
|
||||||
|
this.objects = 0;
|
||||||
|
this.usageByUser = [];
|
||||||
|
this.usageRaw = 0;
|
||||||
|
this.usage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
photos!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
videos!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
objects!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usageRaw!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'string' })
|
||||||
|
usage!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
isArray: true,
|
||||||
|
type: UsageByUserDto,
|
||||||
|
title: 'Array of usage for each user',
|
||||||
|
example: [
|
||||||
|
{
|
||||||
|
photos: 1,
|
||||||
|
videos: 1,
|
||||||
|
objects: 1,
|
||||||
|
diskUsageRaw: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
usageByUser!: UsageByUserDto[];
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class UsageByUserDto {
|
||||||
|
constructor(userId: string) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.objects = 0;
|
||||||
|
this.videos = 0;
|
||||||
|
this.photos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'string' })
|
||||||
|
userId: string;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
objects: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
videos: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
photos: number;
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usageRaw!: number;
|
||||||
|
@ApiProperty({ type: 'string' })
|
||||||
|
usage!: string;
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { ApiTags } from '@nestjs/swagger';
|
|||||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
||||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
||||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||||
|
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||||
|
|
||||||
@ApiTags('Server Info')
|
@ApiTags('Server Info')
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
@ -25,4 +26,9 @@ export class ServerInfoController {
|
|||||||
async getServerVersion(): Promise<ServerVersionReponseDto> {
|
async getServerVersion(): Promise<ServerVersionReponseDto> {
|
||||||
return serverVersion;
|
return serverVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/stats')
|
||||||
|
async getStats(): Promise<ServerStatsResponseDto> {
|
||||||
|
return await this.serverInfoService.getStats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ServerInfoService } from './server-info.service';
|
import { ServerInfoService } from './server-info.service';
|
||||||
import { ServerInfoController } from './server-info.controller';
|
import { ServerInfoController } from './server-info.controller';
|
||||||
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([AssetEntity])],
|
||||||
controllers: [ServerInfoController],
|
controllers: [ServerInfoController],
|
||||||
providers: [ServerInfoService],
|
providers: [ServerInfoService],
|
||||||
})
|
})
|
||||||
|
@ -2,9 +2,21 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||||
import diskusage from 'diskusage';
|
import diskusage from 'diskusage';
|
||||||
|
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||||
|
import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
|
||||||
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import path from 'path';
|
||||||
|
import { readdirSync, statSync } from 'fs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerInfoService {
|
export class ServerInfoService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AssetEntity)
|
||||||
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||||
const diskInfo = await diskusage.check(APP_UPLOAD_LOCATION);
|
const diskInfo = await diskusage.check(APP_UPLOAD_LOCATION);
|
||||||
|
|
||||||
@ -18,7 +30,6 @@ export class ServerInfoService {
|
|||||||
serverInfo.diskSizeRaw = diskInfo.total;
|
serverInfo.diskSizeRaw = diskInfo.total;
|
||||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||||
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,4 +59,61 @@ export class ServerInfoService {
|
|||||||
return `${sizeInByte}B`;
|
return `${sizeInByte}B`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getStats(): Promise<ServerStatsResponseDto> {
|
||||||
|
const res = await this.assetRepository
|
||||||
|
.createQueryBuilder('asset')
|
||||||
|
.select(`COUNT(asset.id)`, 'count')
|
||||||
|
.addSelect(`asset.type`, 'type')
|
||||||
|
.addSelect(`asset.userId`, 'userId')
|
||||||
|
.groupBy('asset.type, asset.userId')
|
||||||
|
.addGroupBy('asset.type')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
const serverStats = new ServerStatsResponseDto();
|
||||||
|
const tmpMap = new Map<string, UsageByUserDto>();
|
||||||
|
const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id);
|
||||||
|
res.map((item) => {
|
||||||
|
const usage: UsageByUserDto = getUsageByUser(item.userId);
|
||||||
|
if (item.type === 'IMAGE') {
|
||||||
|
usage.photos = parseInt(item.count);
|
||||||
|
serverStats.photos += usage.photos;
|
||||||
|
} else if (item.type === 'VIDEO') {
|
||||||
|
usage.videos = parseInt(item.count);
|
||||||
|
serverStats.videos += usage.videos;
|
||||||
|
}
|
||||||
|
tmpMap.set(item.userId, usage);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const userId of tmpMap.keys()) {
|
||||||
|
const usage = getUsageByUser(userId);
|
||||||
|
const userDiskUsage = await ServerInfoService.getDirectoryStats(path.join(APP_UPLOAD_LOCATION, userId));
|
||||||
|
usage.usageRaw = userDiskUsage.size;
|
||||||
|
usage.objects = userDiskUsage.fileCount;
|
||||||
|
usage.usage = ServerInfoService.getHumanReadableString(usage.usageRaw);
|
||||||
|
serverStats.usageRaw += usage.usageRaw;
|
||||||
|
serverStats.objects += usage.objects;
|
||||||
|
}
|
||||||
|
serverStats.usage = ServerInfoService.getHumanReadableString(serverStats.usageRaw);
|
||||||
|
serverStats.usageByUser = Array.from(tmpMap.values());
|
||||||
|
return serverStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getDirectoryStats(dirPath: string) {
|
||||||
|
let size = 0;
|
||||||
|
let fileCount = 0;
|
||||||
|
for (const filename of readdirSync(dirPath)) {
|
||||||
|
const absFilename = path.join(dirPath, filename);
|
||||||
|
const fileStat = statSync(absFilename);
|
||||||
|
if (fileStat.isFile()) {
|
||||||
|
size += fileStat.size;
|
||||||
|
fileCount += 1;
|
||||||
|
} else if (fileStat.isDirectory()) {
|
||||||
|
const subDirStat = await ServerInfoService.getDirectoryStats(absFilename);
|
||||||
|
size += subDirStat.size;
|
||||||
|
fileCount += subDirStat.fileCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { size, fileCount };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@ import { validate } from 'class-validator';
|
|||||||
import { CreateUserDto } from './create-user.dto';
|
import { CreateUserDto } from './create-user.dto';
|
||||||
|
|
||||||
describe('create user DTO', () => {
|
describe('create user DTO', () => {
|
||||||
it('validates the email', async() => {
|
it('validates the email', async () => {
|
||||||
const params: Partial<CreateUserDto> = {
|
const params: Partial<CreateUserDto> = {
|
||||||
email: undefined,
|
email: undefined,
|
||||||
password: 'password',
|
password: 'password',
|
||||||
firstName: 'first name',
|
firstName: 'first name',
|
||||||
lastName: 'last name',
|
lastName: 'last name',
|
||||||
}
|
};
|
||||||
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
|
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
|
||||||
let errors = await validate(dto);
|
let errors = await validate(dto);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
|
@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Not, Repository } from 'typeorm';
|
import { Not, Repository } from 'typeorm';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { UpdateUserDto } from './dto/update-user.dto'
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
|
||||||
export interface IUserRepository {
|
export interface IUserRepository {
|
||||||
get(userId: string): Promise<UserEntity | null>;
|
get(userId: string): Promise<UserEntity | null>;
|
||||||
|
@ -17,8 +17,8 @@ import { UserRepository, USER_REPOSITORY } from './user-repository';
|
|||||||
ImmichJwtService,
|
ImmichJwtService,
|
||||||
{
|
{
|
||||||
provide: USER_REPOSITORY,
|
provide: USER_REPOSITORY,
|
||||||
useClass: UserRepository
|
useClass: UserRepository,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,20 +1,20 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { ConfigModuleOptions } from '@nestjs/config';
|
import { ConfigModuleOptions } from '@nestjs/config';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
import { createSecretKey, generateKeySync } from 'node:crypto'
|
import { createSecretKey, generateKeySync } from 'node:crypto';
|
||||||
|
|
||||||
const jwtSecretValidator: Joi.CustomValidator<string> = (value, ) => {
|
const jwtSecretValidator: Joi.CustomValidator<string> = (value) => {
|
||||||
const key = createSecretKey(value, "base64")
|
const key = createSecretKey(value, 'base64');
|
||||||
const keySizeBits = (key.symmetricKeySize ?? 0) * 8
|
const keySizeBits = (key.symmetricKeySize ?? 0) * 8;
|
||||||
|
|
||||||
if (keySizeBits < 128) {
|
if (keySizeBits < 128) {
|
||||||
const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64')
|
const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64');
|
||||||
Logger.warn("The current JWT_SECRET key is insecure. It should be at least 128 bits long!")
|
Logger.warn('The current JWT_SECRET key is insecure. It should be at least 128 bits long!');
|
||||||
Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`)
|
Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const immichAppConfig: ConfigModuleOptions = {
|
export const immichAppConfig: ConfigModuleOptions = {
|
||||||
envFilePath: '.env',
|
envFilePath: '.env',
|
||||||
@ -26,7 +26,7 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
DB_DATABASE_NAME: Joi.string().required(),
|
DB_DATABASE_NAME: Joi.string().required(),
|
||||||
JWT_SECRET: Joi.string().required().custom(jwtSecretValidator),
|
JWT_SECRET: Joi.string().required().custom(jwtSecretValidator),
|
||||||
DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
|
DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
|
||||||
REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0,1,2,3).default(3),
|
REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3),
|
||||||
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
|
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,7 @@ export class AlbumEntity {
|
|||||||
@CreateDateColumn({ type: 'timestamptz' })
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
|
|
||||||
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true})
|
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
|
||||||
albumThumbnailAssetId!: string | null;
|
albumThumbnailAssetId!: string | null;
|
||||||
|
|
||||||
@OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
|
@OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique} from 'typeorm';
|
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
import { AlbumEntity } from './album.entity';
|
import { AlbumEntity } from './album.entity';
|
||||||
import { AssetEntity } from './asset.entity';
|
import { AssetEntity } from './asset.entity';
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class RenameAssetAlbumIdSequence1656888591977 implements MigrationInterface {
|
export class RenameAssetAlbumIdSequence1656888591977 implements MigrationInterface {
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
|
await queryRunner.query(`alter sequence asset_shared_album_id_seq rename to asset_album_id_seq;`);
|
||||||
await queryRunner.query(`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`);
|
await queryRunner.query(
|
||||||
|
`alter table asset_album alter column id set default nextval('asset_album_id_seq'::regclass);`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
|
await queryRunner.query(`alter sequence asset_album_id_seq rename to asset_shared_album_id_seq;`);
|
||||||
await queryRunner.query(`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`);
|
await queryRunner.query(
|
||||||
|
`alter table asset_album alter column id set default nextval('asset_shared_album_id_seq'::regclass);`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class DropExifTextSearchableColumns1656888918620 implements MigrationInterface {
|
export class DropExifTextSearchableColumns1656888918620 implements MigrationInterface {
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exif_text_searchable_column"`);
|
||||||
}
|
}
|
||||||
@ -30,5 +29,4 @@ export class DropExifTextSearchableColumns1656888918620 implements MigrationInte
|
|||||||
USING GIN (exif_text_searchable_column);
|
USING GIN (exif_text_searchable_column);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class MatchMigrationsWithTypeORMEntities1656889061566 implements MigrationInterface {
|
export class MatchMigrationsWithTypeORMEntities1656889061566 implements MigrationInterface {
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||||
COALESCE(make, '') || ' ' ||
|
COALESCE(make, '') || ' ' ||
|
||||||
@ -12,8 +11,21 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
|
|||||||
COALESCE("state", '') || ' ' ||
|
COALESCE("state", '') || ' ' ||
|
||||||
COALESCE("country", ''))) STORED`);
|
COALESCE("country", ''))) STORED`);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||||
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","exifTextSearchableColumn","postgres","public","exif"]);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["postgres","public","exif","GENERATED_COLUMN","exifTextSearchableColumn","TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))"]);
|
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||||
|
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'postgres', 'public', 'exif'],
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
|
[
|
||||||
|
'postgres',
|
||||||
|
'public',
|
||||||
|
'exif',
|
||||||
|
'GENERATED_COLUMN',
|
||||||
|
'exifTextSearchableColumn',
|
||||||
|
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
|
||||||
|
],
|
||||||
|
);
|
||||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
|
||||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
|
||||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
|
||||||
@ -22,9 +34,15 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
|
|||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`,
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
@ -33,14 +51,22 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
|
|||||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
|
||||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
|
||||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
|
||||||
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","exifTextSearchableColumn","immich","public","exif"]);
|
await queryRunner.query(
|
||||||
|
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||||
|
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||||
|
);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "UQ_unique_asset_in_album"`);
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_a1e2734a1ce361e7a26f6b28288" UNIQUE ("albumId", "assetId")`,
|
||||||
await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_64f2e7d68d1d1d8417acc844a4a" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_a8b79a84996cef6ba6a3662825d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,16 +1,17 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class AddAssetChecksum1661881837496 implements MigrationInterface {
|
export class AddAssetChecksum1661881837496 implements MigrationInterface {
|
||||||
name = 'AddAssetChecksum1661881837496'
|
name = 'AddAssetChecksum1661881837496';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "assets" ADD "checksum" bytea`);
|
await queryRunner.query(`ALTER TABLE "assets" ADD "checksum" bytea`);
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`);
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE 'checksum' IS NOT NULL`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
|
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
|
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements MigrationInterface {
|
export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements MigrationInterface {
|
||||||
name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662'
|
name = 'UpdateAssetTableWithNewUniqueConstraint1661971370662';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e"`);
|
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e"`);
|
||||||
@ -10,7 +10,8 @@ export class UpdateAssetTableWithNewUniqueConstraint1661971370662 implements Mig
|
|||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_userid_checksum"`);
|
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_userid_checksum"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`);
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "assets" ADD CONSTRAINT "UQ_b599ab0bd9574958acb0b30a90e" UNIQUE ("deviceAssetId", "userId", "deviceId")`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1157,6 +1157,49 @@ export interface ServerPingResponse {
|
|||||||
*/
|
*/
|
||||||
'res': string;
|
'res': string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
export interface ServerStatsResponseDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
'photos': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
'videos': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
'objects': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
'usageRaw': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
'usage': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Array<UsageByUserDto>}
|
||||||
|
* @memberof ServerStatsResponseDto
|
||||||
|
*/
|
||||||
|
'usageByUser': Array<UsageByUserDto>;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -1365,6 +1408,49 @@ export interface UpdateUserDto {
|
|||||||
*/
|
*/
|
||||||
'profileImagePath'?: string;
|
'profileImagePath'?: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface UsageByUserDto
|
||||||
|
*/
|
||||||
|
export interface UsageByUserDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UsageByUserDto
|
||||||
|
*/
|
||||||
|
'userId': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UsageByUserDto
|
||||||
|
*/
|
||||||
|
'objects': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UsageByUserDto
|
||||||
|
*/
|
||||||
|
'videos': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UsageByUserDto
|
||||||
|
*/
|
||||||
|
'photos': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UsageByUserDto
|
||||||
|
*/
|
||||||
|
'usageRaw': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UsageByUserDto
|
||||||
|
*/
|
||||||
|
'usage': string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -4132,6 +4218,35 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getStats: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
const localVarPath = `/server-info/stats`;
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
@ -4198,6 +4313,15 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async getStats(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerStatsResponseDto>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getStats(options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
@ -4233,6 +4357,14 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
|
|||||||
getServerVersion(options?: any): AxiosPromise<ServerVersionReponseDto> {
|
getServerVersion(options?: any): AxiosPromise<ServerVersionReponseDto> {
|
||||||
return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
|
return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
getStats(options?: any): AxiosPromise<ServerStatsResponseDto> {
|
||||||
|
return localVarFp.getStats(options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
@ -4271,6 +4403,16 @@ export class ServerInfoApi extends BaseAPI {
|
|||||||
return ServerInfoApiFp(this.configuration).getServerVersion(options).then((request) => request(this.axios, this.basePath));
|
return ServerInfoApiFp(this.configuration).getServerVersion(options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof ServerInfoApi
|
||||||
|
*/
|
||||||
|
public getStats(options?: AxiosRequestConfig) {
|
||||||
|
return ServerInfoApiFp(this.configuration).getStats(options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
|
52
web/src/lib/components/admin-page/server-stats.svelte
Normal file
52
web/src/lib/components/admin-page/server-stats.svelte
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ServerStatsResponseDto, UserResponseDto } from '@api';
|
||||||
|
export let stats: ServerStatsResponseDto;
|
||||||
|
export let allUsers: Array<UserResponseDto>;
|
||||||
|
|
||||||
|
const getFullName = (userId: string) => {
|
||||||
|
let name = 'Admin'; // since we do not have admin user in allUsers
|
||||||
|
allUsers.forEach((user) => {
|
||||||
|
if (user.id === userId) name = `${user.firstName} ${user.lastName}`;
|
||||||
|
});
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="border p-6 rounded-2xl bg-white text-center">
|
||||||
|
<h1 class="font-medium text-immich-primary">Server Usage</h1>
|
||||||
|
<div class="flex flex-row gap-6 mt-4 font-medium">
|
||||||
|
<p class="grow">Photos: {stats.photos}</p>
|
||||||
|
<p class="grow">Videos: {stats.videos}</p>
|
||||||
|
<p class="grow">Objects: {stats.objects}</p>
|
||||||
|
<p class="grow">Size: {stats.usage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border p-6 rounded-2xl bg-white">
|
||||||
|
<h1 class="font-medium text-immich-primary">Usage by User</h1>
|
||||||
|
<table class="text-left w-full mt-4">
|
||||||
|
<!-- table header -->
|
||||||
|
<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12">
|
||||||
|
<tr class="flex w-full place-items-center">
|
||||||
|
<th class="text-center w-1/5 font-medium text-sm">User</th>
|
||||||
|
<th class="text-center w-1/5 font-medium text-sm">Photos</th>
|
||||||
|
<th class="text-center w-1/5 font-medium text-sm">Videos</th>
|
||||||
|
<th class="text-center w-1/5 font-medium text-sm">Objects</th>
|
||||||
|
<th class="text-center w-1/5 font-medium text-sm">Size</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border">
|
||||||
|
{#each stats.usageByUser as user}
|
||||||
|
<tr class="text-center flex place-items-center w-full h-[40px]">
|
||||||
|
<td class="text-sm px-2 w-1/5 text-ellipsis">{getFullName(user.userId)}</td>
|
||||||
|
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.photos}</td>
|
||||||
|
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.videos}</td>
|
||||||
|
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.objects}</td>
|
||||||
|
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.usage}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,7 +1,8 @@
|
|||||||
export enum AdminSideBarSelection {
|
export enum AdminSideBarSelection {
|
||||||
USER_MANAGEMENT = 'User management',
|
USER_MANAGEMENT = 'User management',
|
||||||
JOBS = 'Jobs',
|
JOBS = 'Jobs',
|
||||||
SETTINGS = 'Settings'
|
SETTINGS = 'Settings',
|
||||||
|
STATS = 'Server Stats'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppSideBarSelection {
|
export enum AppSideBarSelection {
|
||||||
|
@ -12,8 +12,10 @@ export const load: PageServerLoad = async ({ parent }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
|
const { data: allUsers } = await serverApi.userApi.getAllUsers(false);
|
||||||
|
const { data: stats } = await serverApi.serverInfoApi.getStats();
|
||||||
return {
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
allUsers: allUsers
|
allUsers: allUsers,
|
||||||
|
stats: stats
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
|
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
|
||||||
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
||||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||||
|
import Server from 'svelte-material-icons/Server.svelte';
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||||
import UserManagement from '$lib/components/admin-page/user-management.svelte';
|
import UserManagement from '$lib/components/admin-page/user-management.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
@ -14,6 +15,7 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { api, UserResponseDto } from '@api';
|
import { api, UserResponseDto } from '@api';
|
||||||
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
|
import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
|
||||||
|
import ServerStats from '$lib/components/admin-page/server-stats.svelte';
|
||||||
|
|
||||||
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
|
||||||
|
|
||||||
@ -121,6 +123,13 @@
|
|||||||
isSelected={selectedAction === AdminSideBarSelection.JOBS}
|
isSelected={selectedAction === AdminSideBarSelection.JOBS}
|
||||||
on:selected={onButtonClicked}
|
on:selected={onButtonClicked}
|
||||||
/>
|
/>
|
||||||
|
<SideBarButton
|
||||||
|
title="Server Stats"
|
||||||
|
logo={Server}
|
||||||
|
actionType={AdminSideBarSelection.STATS}
|
||||||
|
isSelected={selectedAction === AdminSideBarSelection.STATS}
|
||||||
|
on:selected={onButtonClicked}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="mb-6 mt-auto">
|
<div class="mb-6 mt-auto">
|
||||||
<StatusBox />
|
<StatusBox />
|
||||||
@ -144,6 +153,9 @@
|
|||||||
{#if selectedAction === AdminSideBarSelection.JOBS}
|
{#if selectedAction === AdminSideBarSelection.JOBS}
|
||||||
<JobsPanel />
|
<JobsPanel />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if selectedAction === AdminSideBarSelection.STATS}
|
||||||
|
<ServerStats stats={data.stats} allUsers={data.allUsers} />
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
Loading…
Reference in New Issue
Block a user