1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web,server): server features (#3756)

* feat: server features

* chore: open api

* icon size

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2023-08-18 00:55:26 -04:00 committed by GitHub
parent 28d3d3e679
commit 2b839088c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 489 additions and 156 deletions

View File

@ -2087,6 +2087,43 @@ export interface SearchResponseDto {
*/
'assets': SearchAssetResponseDto;
}
/**
*
* @export
* @interface ServerFeaturesDto
*/
export interface ServerFeaturesDto {
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'machineLearning': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'oauth': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'oauthAutoLaunch': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'passwordLogin': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'search': boolean;
}
/**
*
* @export
@ -2208,25 +2245,25 @@ export interface ServerStatsResponseDto {
/**
*
* @export
* @interface ServerVersionReponseDto
* @interface ServerVersionResponseDto
*/
export interface ServerVersionReponseDto {
export interface ServerVersionResponseDto {
/**
*
* @type {number}
* @memberof ServerVersionReponseDto
* @memberof ServerVersionResponseDto
*/
'major': number;
/**
*
* @type {number}
* @memberof ServerVersionReponseDto
* @memberof ServerVersionResponseDto
*/
'minor': number;
/**
*
* @type {number}
* @memberof ServerVersionReponseDto
* @memberof ServerVersionResponseDto
*/
'patch': number;
}
@ -10156,6 +10193,35 @@ export class SearchApi extends BaseAPI {
*/
export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerFeatures: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/server-info/features`;
// 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);
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.
@ -10329,6 +10395,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
export const ServerInfoApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getServerFeatures(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerFeaturesDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerFeatures(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
@ -10343,7 +10418,7 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getServerVersion(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerVersionReponseDto>> {
async getServerVersion(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerVersionResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -10384,6 +10459,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ServerInfoApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerFeatures(options?: AxiosRequestConfig): AxiosPromise<ServerFeaturesDto> {
return localVarFp.getServerFeatures(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
@ -10397,7 +10480,7 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerVersion(options?: AxiosRequestConfig): AxiosPromise<ServerVersionReponseDto> {
getServerVersion(options?: AxiosRequestConfig): AxiosPromise<ServerVersionResponseDto> {
return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
},
/**
@ -10434,6 +10517,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @extends {BaseAPI}
*/
export class ServerInfoApi extends BaseAPI {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ServerInfoApi
*/
public getServerFeatures(options?: AxiosRequestConfig) {
return ServerInfoApiFp(this.configuration).getServerFeatures(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.

View File

@ -4,14 +4,14 @@ import { SessionService } from '../services/session.service';
import { LoginError } from '../cores/errors/login-error';
import { exit } from 'node:process';
import os from 'os';
import { ServerVersionReponseDto, UserResponseDto } from 'src/api/open-api';
import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
export abstract class BaseCommand {
protected sessionService!: SessionService;
protected immichApi!: ImmichApi;
protected deviceId!: string;
protected user!: UserResponseDto;
protected serverVersion!: ServerVersionReponseDto;
protected serverVersion!: ServerVersionResponseDto;
protected configDir;
protected authPath;

View File

@ -1,7 +1,7 @@
import 'package:openapi/api.dart';
class ServerInfoState {
final ServerVersionReponseDto serverVersion;
final ServerVersionResponseDto serverVersion;
final bool isVersionMismatch;
final String versionMismatchErrorMessage;
@ -12,7 +12,7 @@ class ServerInfoState {
});
ServerInfoState copyWith({
ServerVersionReponseDto? serverVersion,
ServerVersionResponseDto? serverVersion,
bool? isVersionMismatch,
String? versionMismatchErrorMessage,
}) {

View File

@ -10,7 +10,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
ServerInfoNotifier(this._serverInfoService)
: super(
ServerInfoState(
serverVersion: ServerVersionReponseDto(
serverVersion: ServerVersionResponseDto(
major: 0,
patch_: 0,
minor: 0,
@ -23,7 +23,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
final ServerInfoService _serverInfoService;
getServerVersion() async {
ServerVersionReponseDto? serverVersion =
ServerVersionResponseDto? serverVersion =
await _serverInfoService.getServerVersion();
if (serverVersion == null) {

View File

@ -24,7 +24,7 @@ class ServerInfoService {
}
}
Future<ServerVersionReponseDto?> getServerVersion() async {
Future<ServerVersionResponseDto?> getServerVersion() async {
try {
return await _apiService.serverInfoApi.getServerVersion();
} catch (e) {

View File

@ -85,12 +85,13 @@ doc/SearchExploreResponseDto.md
doc/SearchFacetCountResponseDto.md
doc/SearchFacetResponseDto.md
doc/SearchResponseDto.md
doc/ServerFeaturesDto.md
doc/ServerInfoApi.md
doc/ServerInfoResponseDto.md
doc/ServerMediaTypesResponseDto.md
doc/ServerPingResponse.md
doc/ServerStatsResponseDto.md
doc/ServerVersionReponseDto.md
doc/ServerVersionResponseDto.md
doc/SharedLinkApi.md
doc/SharedLinkCreateDto.md
doc/SharedLinkEditDto.md
@ -223,11 +224,12 @@ lib/model/search_explore_response_dto.dart
lib/model/search_facet_count_response_dto.dart
lib/model/search_facet_response_dto.dart
lib/model/search_response_dto.dart
lib/model/server_features_dto.dart
lib/model/server_info_response_dto.dart
lib/model/server_media_types_response_dto.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_response_dto.dart
lib/model/shared_link_create_dto.dart
lib/model/shared_link_edit_dto.dart
lib/model/shared_link_response_dto.dart
@ -342,12 +344,13 @@ test/search_explore_response_dto_test.dart
test/search_facet_count_response_dto_test.dart
test/search_facet_response_dto_test.dart
test/search_response_dto_test.dart
test/server_features_dto_test.dart
test/server_info_api_test.dart
test/server_info_response_dto_test.dart
test/server_media_types_response_dto_test.dart
test/server_ping_response_test.dart
test/server_stats_response_dto_test.dart
test/server_version_reponse_dto_test.dart
test/server_version_response_dto_test.dart
test/shared_link_api_test.dart
test/shared_link_create_dto_test.dart
test/shared_link_edit_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

BIN
mobile/openapi/doc/ServerFeaturesDto.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3248,6 +3248,27 @@
]
}
},
"/server-info/features": {
"get": {
"operationId": "getServerFeatures",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerFeaturesDto"
}
}
},
"description": ""
}
},
"tags": [
"Server Info"
]
}
},
"/server-info/media-types": {
"get": {
"operationId": "getSupportedMediaTypes",
@ -3331,7 +3352,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerVersionReponseDto"
"$ref": "#/components/schemas/ServerVersionResponseDto"
}
}
},
@ -6331,6 +6352,33 @@
],
"type": "object"
},
"ServerFeaturesDto": {
"properties": {
"machineLearning": {
"type": "boolean"
},
"oauth": {
"type": "boolean"
},
"oauthAutoLaunch": {
"type": "boolean"
},
"passwordLogin": {
"type": "boolean"
},
"search": {
"type": "boolean"
}
},
"required": [
"machineLearning",
"search",
"oauth",
"oauthAutoLaunch",
"passwordLogin"
],
"type": "object"
},
"ServerInfoResponseDto": {
"properties": {
"diskAvailable": {
@ -6450,7 +6498,7 @@
],
"type": "object"
},
"ServerVersionReponseDto": {
"ServerVersionResponseDto": {
"properties": {
"major": {
"type": "integer"

View File

@ -21,6 +21,8 @@ export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${s
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
export const SEARCH_ENABLED = process.env.TYPESENSE_ENABLED !== 'false';
export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false';

View File

@ -1,2 +1,2 @@
export * from './response-dto';
export * from './server-info.dto';
export * from './server-info.service';

View File

@ -1,5 +0,0 @@
export * from './server-info-response.dto';
export * from './server-ping-response.dto';
export * from './server-stats-response.dto';
export * from './server-version-response.dto';
export * from './usage-by-user-response.dto';

View File

@ -1,19 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class ServerInfoResponseDto {
diskSize!: string;
diskUse!: string;
diskAvailable!: string;
@ApiProperty({ type: 'integer', format: 'int64' })
diskSizeRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
diskUseRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
diskAvailableRaw!: number;
@ApiProperty({ type: 'number', format: 'float' })
diskUsagePercentage!: number;
}

View File

@ -1,10 +0,0 @@
import { ApiResponseProperty } from '@nestjs/swagger';
export class ServerPingResponse {
constructor(res: string) {
this.res = res;
}
@ApiResponseProperty({ type: String, example: 'pong' })
res!: string;
}

View File

@ -1,33 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { UsageByUserDto } from './usage-by-user-response.dto';
export class ServerStatsResponseDto {
@ApiProperty({ type: 'integer' })
photos = 0;
@ApiProperty({ type: 'integer' })
videos = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
usage = 0;
@ApiProperty({
isArray: true,
type: UsageByUserDto,
title: 'Array of usage for each user',
example: [
{
photos: 1,
videos: 1,
diskUsageRaw: 1,
},
],
})
usageByUser: UsageByUserDto[] = [];
}
export class ServerMediaTypesResponseDto {
video!: string[];
image!: string[];
sidecar!: string[];
}

View File

@ -1,11 +0,0 @@
import { IServerVersion } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger';
export class ServerVersionReponseDto implements IServerVersion {
@ApiProperty({ type: 'integer' })
major!: number;
@ApiProperty({ type: 'integer' })
minor!: number;
@ApiProperty({ type: 'integer' })
patch!: number;
}

View File

@ -1,16 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class UsageByUserDto {
@ApiProperty({ type: 'string' })
userId!: string;
@ApiProperty({ type: 'string' })
userFirstName!: string;
@ApiProperty({ type: 'string' })
userLastName!: string;
@ApiProperty({ type: 'integer' })
photos!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
usage!: number;
}

View File

@ -0,0 +1,89 @@
import { IServerVersion } from '@app/domain';
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
export class ServerPingResponse {
@ApiResponseProperty({ type: String, example: 'pong' })
res!: string;
}
export class ServerInfoResponseDto {
diskSize!: string;
diskUse!: string;
diskAvailable!: string;
@ApiProperty({ type: 'integer', format: 'int64' })
diskSizeRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
diskUseRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
diskAvailableRaw!: number;
@ApiProperty({ type: 'number', format: 'float' })
diskUsagePercentage!: number;
}
export class ServerVersionResponseDto implements IServerVersion {
@ApiProperty({ type: 'integer' })
major!: number;
@ApiProperty({ type: 'integer' })
minor!: number;
@ApiProperty({ type: 'integer' })
patch!: number;
}
export class UsageByUserDto {
@ApiProperty({ type: 'string' })
userId!: string;
@ApiProperty({ type: 'string' })
userFirstName!: string;
@ApiProperty({ type: 'string' })
userLastName!: string;
@ApiProperty({ type: 'integer' })
photos!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
usage!: number;
}
export class ServerStatsResponseDto {
@ApiProperty({ type: 'integer' })
photos = 0;
@ApiProperty({ type: 'integer' })
videos = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
usage = 0;
@ApiProperty({
isArray: true,
type: UsageByUserDto,
title: 'Array of usage for each user',
example: [
{
photos: 1,
videos: 1,
diskUsageRaw: 1,
},
],
})
usageByUser: UsageByUserDto[] = [];
}
export class ServerMediaTypesResponseDto {
video!: string[];
image!: string[];
sidecar!: string[];
}
export class ServerFeaturesDto {
machineLearning!: boolean;
search!: boolean;
oauth!: boolean;
oauthAutoLaunch!: boolean;
passwordLogin!: boolean;
}

View File

@ -1,19 +1,22 @@
import { newStorageRepositoryMock, newUserRepositoryMock } from '@test';
import { newStorageRepositoryMock, newSystemConfigRepositoryMock, newUserRepositoryMock } from '@test';
import { serverVersion } from '../domain.constant';
import { ISystemConfigRepository } from '../index';
import { IStorageRepository } from '../storage';
import { IUserRepository } from '../user';
import { ServerInfoService } from './server-info.service';
describe(ServerInfoService.name, () => {
let sut: ServerInfoService;
let configMock: jest.Mocked<ISystemConfigRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => {
configMock = newSystemConfigRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
sut = new ServerInfoService(userMock, storageMock);
sut = new ServerInfoService(configMock, userMock, storageMock);
});
it('should work', () => {
@ -140,6 +143,19 @@ describe(ServerInfoService.name, () => {
it('should respond the server version', () => {
expect(sut.getVersion()).toEqual(serverVersion);
});
describe('getFeatures', () => {
it('should respond the server features', async () => {
await expect(sut.getFeatures()).resolves.toEqual({
machineLearning: true,
oauth: false,
oauthAutoLaunch: false,
passwordLogin: true,
search: true,
});
expect(configMock.load).toHaveBeenCalled();
});
});
});
describe('getStats', () => {

View File

@ -1,24 +1,31 @@
import { Inject, Injectable } from '@nestjs/common';
import { mimeTypes, serverVersion } from '../domain.constant';
import { MACHINE_LEARNING_ENABLED, mimeTypes, SEARCH_ENABLED, serverVersion } from '../domain.constant';
import { asHumanReadable } from '../domain.util';
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
import { ISystemConfigRepository } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { IUserRepository, UserStatsQueryResponse } from '../user';
import {
ServerFeaturesDto,
ServerInfoResponseDto,
ServerMediaTypesResponseDto,
ServerPingResponse,
ServerStatsResponseDto,
UsageByUserDto,
} from './response-dto';
} from './server-info.dto';
@Injectable()
export class ServerInfoService {
private storageCore = new StorageCore();
private configCore: SystemConfigCore;
constructor(
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {}
) {
this.configCore = new SystemConfigCore(configRepository);
}
async getInfo(): Promise<ServerInfoResponseDto> {
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
@ -38,13 +45,27 @@ export class ServerInfoService {
}
ping(): ServerPingResponse {
return new ServerPingResponse('pong');
return { res: 'pong' };
}
getVersion() {
return serverVersion;
}
async getFeatures(): Promise<ServerFeaturesDto> {
const config = await this.configCore.getConfig();
return {
machineLearning: MACHINE_LEARNING_ENABLED,
search: SEARCH_ENABLED,
// TODO: use these instead of `POST oauth/config`
oauth: config.oauth.enabled,
oauthAutoLaunch: config.oauth.autoLaunch,
passwordLogin: config.passwordLogin.enabled,
};
}
async getStats(): Promise<ServerStatsResponseDto> {
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
const serverStats = new ServerStatsResponseDto();

View File

@ -1,10 +1,11 @@
import {
ServerFeaturesDto,
ServerInfoResponseDto,
ServerInfoService,
ServerMediaTypesResponseDto,
ServerPingResponse,
ServerStatsResponseDto,
ServerVersionReponseDto,
ServerVersionResponseDto,
} from '@app/domain';
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@ -24,25 +25,31 @@ export class ServerInfoController {
}
@PublicRoute()
@Get('/ping')
@Get('ping')
pingServer(): ServerPingResponse {
return this.service.ping();
}
@PublicRoute()
@Get('/version')
getServerVersion(): ServerVersionReponseDto {
@Get('version')
getServerVersion(): ServerVersionResponseDto {
return this.service.getVersion();
}
@PublicRoute()
@Get('features')
getServerFeatures(): Promise<ServerFeaturesDto> {
return this.service.getFeatures();
}
@AdminRoute()
@Get('/stats')
@Get('stats')
getStats(): Promise<ServerStatsResponseDto> {
return this.service.getStats();
}
@PublicRoute()
@Get('/media-types')
@Get('media-types')
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return this.service.getSupportedMediaTypes();
}

View File

@ -2087,6 +2087,43 @@ export interface SearchResponseDto {
*/
'assets': SearchAssetResponseDto;
}
/**
*
* @export
* @interface ServerFeaturesDto
*/
export interface ServerFeaturesDto {
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'machineLearning': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'oauth': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'oauthAutoLaunch': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'passwordLogin': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'search': boolean;
}
/**
*
* @export
@ -2208,25 +2245,25 @@ export interface ServerStatsResponseDto {
/**
*
* @export
* @interface ServerVersionReponseDto
* @interface ServerVersionResponseDto
*/
export interface ServerVersionReponseDto {
export interface ServerVersionResponseDto {
/**
*
* @type {number}
* @memberof ServerVersionReponseDto
* @memberof ServerVersionResponseDto
*/
'major': number;
/**
*
* @type {number}
* @memberof ServerVersionReponseDto
* @memberof ServerVersionResponseDto
*/
'minor': number;
/**
*
* @type {number}
* @memberof ServerVersionReponseDto
* @memberof ServerVersionResponseDto
*/
'patch': number;
}
@ -10156,6 +10193,35 @@ export class SearchApi extends BaseAPI {
*/
export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerFeatures: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/server-info/features`;
// 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);
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.
@ -10329,6 +10395,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
export const ServerInfoApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getServerFeatures(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerFeaturesDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerFeatures(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
@ -10343,7 +10418,7 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getServerVersion(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerVersionReponseDto>> {
async getServerVersion(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerVersionResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -10384,6 +10459,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ServerInfoApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerFeatures(options?: AxiosRequestConfig): AxiosPromise<ServerFeaturesDto> {
return localVarFp.getServerFeatures(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
@ -10397,7 +10480,7 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerVersion(options?: AxiosRequestConfig): AxiosPromise<ServerVersionReponseDto> {
getServerVersion(options?: AxiosRequestConfig): AxiosPromise<ServerVersionResponseDto> {
return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
},
/**
@ -10434,6 +10517,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @extends {BaseAPI}
*/
export class ServerInfoApi extends BaseAPI {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ServerInfoApi
*/
public getServerFeatures(options?: AxiosRequestConfig) {
return ServerInfoApiFp(this.configuration).getServerFeatures(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.

View File

@ -4,17 +4,23 @@
<script lang="ts">
export let color: Colors;
export let disabled = false;
const colorClasses: Record<Colors, string> = {
'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
gray: 'bg-gray-300 dark:bg-gray-600',
};
const hoverClasses = disabled
? 'cursor-not-allowed'
: 'hover:bg-immich-primary hover:text-white dark:hover:bg-immich-dark-primary dark:hover:text-black';
</script>
<button
class="flex h-full w-full flex-col place-content-center place-items-center gap-2 px-8 py-2 text-xs text-gray-600 transition-colors hover:bg-immich-primary hover:text-white dark:text-gray-200 dark:hover:bg-immich-dark-primary dark:hover:text-black {colorClasses[
{disabled}
class="flex h-full w-full flex-col place-content-center place-items-center gap-2 px-8 py-2 text-xs text-gray-600 transition-colors dark:text-gray-200 {colorClasses[
color
]}"
]} {hoverClasses}"
on:click
>
<slot />

View File

@ -6,6 +6,7 @@
import FastForward from 'svelte-material-icons/FastForward.svelte';
import AllInclusive from 'svelte-material-icons/AllInclusive.svelte';
import Close from 'svelte-material-icons/Close.svelte';
import AlertCircle from 'svelte-material-icons/AlertCircle.svelte';
import { locale } from '$lib/stores/preferences.store';
import { createEventDispatcher } from 'svelte';
import { JobCommand, JobCommandDto, JobCountsDto, QueueStatusDto } from '@api';
@ -19,6 +20,7 @@
export let queueStatus: QueueStatusDto;
export let allowForceCommand = true;
export let icon: typeof Icon;
export let disabled = false;
export let allText: string;
export let missingText: string;
@ -94,7 +96,15 @@
</div>
</div>
<div class="flex w-full flex-row overflow-hidden sm:w-32 sm:flex-col">
{#if !isIdle}
{#if disabled}
<JobTileButton
disabled={true}
color="light-gray"
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
>
<AlertCircle size="36" /> DISABLED
</JobTileButton>
{:else if !isIdle}
{#if waitingCount > 0}
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
<Close size="24" /> CLEAR

View File

@ -4,6 +4,7 @@
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
import { featureFlags } from '$lib/stores/feature-flags.store';
import { handleError } from '$lib/utils/handle-error';
import { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api';
import type { ComponentType } from 'svelte';
@ -29,6 +30,7 @@
subtitle?: string;
allText?: string;
missingText?: string;
disabled?: boolean;
icon: typeof Icon;
allowForceCommand?: boolean;
component?: ComponentType;
@ -51,7 +53,7 @@
handleCommand(JobName.RecognizeFaces, { command: JobCommand.Start, force: true });
};
const jobDetails: Partial<Record<JobName, JobDetails>> = {
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
[JobName.ThumbnailGeneration]: {
icon: FileJpgBox,
title: api.getJobName(JobName.ThumbnailGeneration),
@ -73,17 +75,20 @@
icon: TagMultiple,
title: api.getJobName(JobName.ObjectTagging),
subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
disabled: !$featureFlags.machineLearning,
},
[JobName.ClipEncoding]: {
icon: VectorCircle,
title: api.getJobName(JobName.ClipEncoding),
subtitle: 'Run machine learning to generate clip embeddings',
disabled: !$featureFlags.machineLearning,
},
[JobName.RecognizeFaces]: {
icon: FaceRecognition,
title: api.getJobName(JobName.RecognizeFaces),
subtitle: 'Run machine learning to recognize faces',
handleCommand: handleFaceCommand,
disabled: !$featureFlags.machineLearning,
},
[JobName.VideoConversion]: {
icon: Video,
@ -97,8 +102,7 @@
component: StorageMigrationDescription,
},
};
const jobDetailsArray = Object.entries(jobDetails) as [JobName, JobDetails][];
$: jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) {
const title = jobDetails[jobId]?.title;
@ -138,11 +142,12 @@
</Button>
</a>
</div>
{#each jobDetailsArray as [jobName, { title, subtitle, allText, missingText, allowForceCommand, icon, component, handleCommand: handleCommandOverride }]}
{#each jobList as [jobName, { title, subtitle, disabled, allText, missingText, allowForceCommand, icon, component, handleCommand: handleCommandOverride }]}
{@const { jobCounts, queueStatus } = jobs[jobName]}
<JobTile
{icon}
{title}
{disabled}
{subtitle}
allText={allText || 'ALL'}
missingText={missingText || 'MISSING'}

View File

@ -16,6 +16,7 @@
import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
import UserAvatar from '../user-avatar.svelte';
import { featureFlags } from '$lib/stores/feature-flags.store';
export let user: UserResponseDto;
export let showUploadButton = true;
@ -45,17 +46,21 @@
</a>
<div class="flex justify-between gap-16 pr-6">
<div class="hidden w-full max-w-5xl flex-1 pl-4 sm:block">
<SearchBar grayTheme={true} />
{#if $featureFlags.search}
<SearchBar grayTheme={true} />
{/if}
</div>
<section class="flex place-items-center justify-end gap-4 max-sm:w-full">
<a href={AppRoute.SEARCH} id="search-button" class="pl-4 sm:hidden">
<IconButton title="Search">
<div class="flex gap-2">
<Magnify size="1.5em" />
</div>
</IconButton>
</a>
{#if $featureFlags.search}
<a href={AppRoute.SEARCH} id="search-button" class="pl-4 sm:hidden">
<IconButton title="Search">
<div class="flex gap-2">
<Magnify size="1.5em" />
</div>
</IconButton>
</a>
{/if}
<ThemeButton />

View File

@ -17,6 +17,7 @@
import SideBarButton from './side-bar-button.svelte';
import { locale } from '$lib/stores/preferences.store';
import SideBarSection from './side-bar-section.svelte';
import { featureFlags } from '$lib/stores/feature-flags.store';
const getStats = async (dto: AssetApiGetAssetStatsRequest) => {
const { data: stats } = await api.assetApi.getAssetStats(dto);
@ -56,9 +57,11 @@
</svelte:fragment>
</SideBarButton>
</a>
<a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.EXPLORE} draggable="false">
<SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
</a>
{#if $featureFlags.search}
<a data-sveltekit-preload-data="hover" data-sveltekit-noscroll href={AppRoute.EXPLORE} draggable="false">
<SideBarButton title="Explore" logo={Magnify} isSelected={$page.route.id === '/(user)/explore'} />
</a>
{/if}
<a data-sveltekit-preload-data="hover" href={AppRoute.MAP} draggable="false">
<SideBarButton title="Map" logo={Map} isSelected={$page.route.id === '/(user)/map'} />
</a>

View File

@ -2,16 +2,16 @@
import { getGithubVersion } from '$lib/utils/get-github-version';
import { onMount } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte';
import type { ServerVersionReponseDto } from '@api';
import type { ServerVersionResponseDto } from '@api';
import Button from '../elements/buttons/button.svelte';
export let serverVersion: ServerVersionReponseDto;
export let serverVersion: ServerVersionResponseDto;
let showModal = false;
let githubVersion: string;
$: serverVersionName = semverToName(serverVersion);
function semverToName({ major, minor, patch }: ServerVersionReponseDto) {
function semverToName({ major, minor, patch }: ServerVersionResponseDto) {
return `v${major}.${minor}.${patch}`;
}

View File

@ -0,0 +1,17 @@
import { api, ServerFeaturesDto } from '@api';
import { writable } from 'svelte/store';
export type FeatureFlags = ServerFeaturesDto;
export const featureFlags = writable<FeatureFlags>({
machineLearning: true,
search: true,
oauth: true,
oauthAutoLaunch: true,
passwordLogin: true,
});
export const loadFeatureFlags = async () => {
const { data } = await api.serverInfoApi.getServerFeatures();
featureFlags.update(() => data);
};

View File

@ -1,6 +1,5 @@
<script lang="ts">
import '../app.css';
import { page } from '$app/stores';
import { afterNavigate, beforeNavigate } from '$app/navigation';
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
@ -14,7 +13,9 @@
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
import FaviconHeader from '$lib/components/shared-components/favicon-header.svelte';
import { onMount } from 'svelte';
import { loadFeatureFlags } from '$lib/stores/feature-flags.store';
import { handleError } from '$lib/utils/handle-error';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
let showNavigationLoadingBar = false;
@ -29,6 +30,14 @@
showNavigationLoadingBar = false;
});
onMount(async () => {
try {
await loadFeatureFlags();
} catch (error) {
handleError(error, 'Unable to load feature flags');
}
});
const dropHandler = async ({ dataTransfer }: DragEvent) => {
const files = dataTransfer?.files;
if (!files) {