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; '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 * @export
@ -2208,25 +2245,25 @@ export interface ServerStatsResponseDto {
/** /**
* *
* @export * @export
* @interface ServerVersionReponseDto * @interface ServerVersionResponseDto
*/ */
export interface ServerVersionReponseDto { export interface ServerVersionResponseDto {
/** /**
* *
* @type {number} * @type {number}
* @memberof ServerVersionReponseDto * @memberof ServerVersionResponseDto
*/ */
'major': number; 'major': number;
/** /**
* *
* @type {number} * @type {number}
* @memberof ServerVersionReponseDto * @memberof ServerVersionResponseDto
*/ */
'minor': number; 'minor': number;
/** /**
* *
* @type {number} * @type {number}
* @memberof ServerVersionReponseDto * @memberof ServerVersionResponseDto
*/ */
'patch': number; 'patch': number;
} }
@ -10156,6 +10193,35 @@ export class SearchApi extends BaseAPI {
*/ */
export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) { export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
return { 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. * @param {*} [options] Override http request option.
@ -10329,6 +10395,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
export const ServerInfoApiFp = function(configuration?: Configuration) { export const ServerInfoApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
return { 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. * @param {*} [options] Override http request option.
@ -10343,7 +10418,7 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.getServerVersion(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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) { export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ServerInfoApiFp(configuration) const localVarFp = ServerInfoApiFp(configuration)
return { 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. * @param {*} [options] Override http request option.
@ -10397,7 +10480,7 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getServerVersion(options?: AxiosRequestConfig): AxiosPromise<ServerVersionReponseDto> { getServerVersion(options?: AxiosRequestConfig): AxiosPromise<ServerVersionResponseDto> {
return localVarFp.getServerVersion(options).then((request) => request(axios, basePath)); return localVarFp.getServerVersion(options).then((request) => request(axios, basePath));
}, },
/** /**
@ -10434,6 +10517,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class ServerInfoApi 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. * @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 { LoginError } from '../cores/errors/login-error';
import { exit } from 'node:process'; import { exit } from 'node:process';
import os from 'os'; import os from 'os';
import { ServerVersionReponseDto, UserResponseDto } from 'src/api/open-api'; import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
export abstract class BaseCommand { export abstract class BaseCommand {
protected sessionService!: SessionService; protected sessionService!: SessionService;
protected immichApi!: ImmichApi; protected immichApi!: ImmichApi;
protected deviceId!: string; protected deviceId!: string;
protected user!: UserResponseDto; protected user!: UserResponseDto;
protected serverVersion!: ServerVersionReponseDto; protected serverVersion!: ServerVersionResponseDto;
protected configDir; protected configDir;
protected authPath; protected authPath;

View file

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

View file

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

View file

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

View file

@ -85,12 +85,13 @@ doc/SearchExploreResponseDto.md
doc/SearchFacetCountResponseDto.md doc/SearchFacetCountResponseDto.md
doc/SearchFacetResponseDto.md doc/SearchFacetResponseDto.md
doc/SearchResponseDto.md doc/SearchResponseDto.md
doc/ServerFeaturesDto.md
doc/ServerInfoApi.md doc/ServerInfoApi.md
doc/ServerInfoResponseDto.md doc/ServerInfoResponseDto.md
doc/ServerMediaTypesResponseDto.md doc/ServerMediaTypesResponseDto.md
doc/ServerPingResponse.md doc/ServerPingResponse.md
doc/ServerStatsResponseDto.md doc/ServerStatsResponseDto.md
doc/ServerVersionReponseDto.md doc/ServerVersionResponseDto.md
doc/SharedLinkApi.md doc/SharedLinkApi.md
doc/SharedLinkCreateDto.md doc/SharedLinkCreateDto.md
doc/SharedLinkEditDto.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_count_response_dto.dart
lib/model/search_facet_response_dto.dart lib/model/search_facet_response_dto.dart
lib/model/search_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_info_response_dto.dart
lib/model/server_media_types_response_dto.dart lib/model/server_media_types_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_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_create_dto.dart
lib/model/shared_link_edit_dto.dart lib/model/shared_link_edit_dto.dart
lib/model/shared_link_response_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_count_response_dto_test.dart
test/search_facet_response_dto_test.dart test/search_facet_response_dto_test.dart
test/search_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_api_test.dart
test/server_info_response_dto_test.dart test/server_info_response_dto_test.dart
test/server_media_types_response_dto_test.dart test/server_media_types_response_dto_test.dart
test/server_ping_response_test.dart test/server_ping_response_test.dart
test/server_stats_response_dto_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_api_test.dart
test/shared_link_create_dto_test.dart test/shared_link_create_dto_test.dart
test/shared_link_edit_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": { "/server-info/media-types": {
"get": { "get": {
"operationId": "getSupportedMediaTypes", "operationId": "getSupportedMediaTypes",
@ -3331,7 +3352,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/ServerVersionReponseDto" "$ref": "#/components/schemas/ServerVersionResponseDto"
} }
} }
}, },
@ -6331,6 +6352,33 @@
], ],
"type": "object" "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": { "ServerInfoResponseDto": {
"properties": { "properties": {
"diskAvailable": { "diskAvailable": {
@ -6450,7 +6498,7 @@
], ],
"type": "object" "type": "object"
}, },
"ServerVersionReponseDto": { "ServerVersionResponseDto": {
"properties": { "properties": {
"major": { "major": {
"type": "integer" "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 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_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false'; 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'; 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 { serverVersion } from '../domain.constant';
import { ISystemConfigRepository } from '../index';
import { IStorageRepository } from '../storage'; import { IStorageRepository } from '../storage';
import { IUserRepository } from '../user'; import { IUserRepository } from '../user';
import { ServerInfoService } from './server-info.service'; import { ServerInfoService } from './server-info.service';
describe(ServerInfoService.name, () => { describe(ServerInfoService.name, () => {
let sut: ServerInfoService; let sut: ServerInfoService;
let configMock: jest.Mocked<ISystemConfigRepository>;
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>; let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newSystemConfigRepositoryMock();
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock(); userMock = newUserRepositoryMock();
sut = new ServerInfoService(userMock, storageMock); sut = new ServerInfoService(configMock, userMock, storageMock);
}); });
it('should work', () => { it('should work', () => {
@ -140,6 +143,19 @@ describe(ServerInfoService.name, () => {
it('should respond the server version', () => { it('should respond the server version', () => {
expect(sut.getVersion()).toEqual(serverVersion); 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', () => { describe('getStats', () => {

View file

@ -1,24 +1,31 @@
import { Inject, Injectable } from '@nestjs/common'; 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 { asHumanReadable } from '../domain.util';
import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; 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 { IUserRepository, UserStatsQueryResponse } from '../user';
import { import {
ServerFeaturesDto,
ServerInfoResponseDto, ServerInfoResponseDto,
ServerMediaTypesResponseDto, ServerMediaTypesResponseDto,
ServerPingResponse, ServerPingResponse,
ServerStatsResponseDto, ServerStatsResponseDto,
UsageByUserDto, UsageByUserDto,
} from './response-dto'; } from './server-info.dto';
@Injectable() @Injectable()
export class ServerInfoService { export class ServerInfoService {
private storageCore = new StorageCore(); private storageCore = new StorageCore();
private configCore: SystemConfigCore;
constructor( constructor(
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {} ) {
this.configCore = new SystemConfigCore(configRepository);
}
async getInfo(): Promise<ServerInfoResponseDto> { async getInfo(): Promise<ServerInfoResponseDto> {
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY); const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
@ -38,13 +45,27 @@ export class ServerInfoService {
} }
ping(): ServerPingResponse { ping(): ServerPingResponse {
return new ServerPingResponse('pong'); return { res: 'pong' };
} }
getVersion() { getVersion() {
return serverVersion; 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> { async getStats(): Promise<ServerStatsResponseDto> {
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
const serverStats = new ServerStatsResponseDto(); const serverStats = new ServerStatsResponseDto();

View file

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

View file

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

View file

@ -4,17 +4,23 @@
<script lang="ts"> <script lang="ts">
export let color: Colors; export let color: Colors;
export let disabled = false;
const colorClasses: Record<Colors, string> = { const colorClasses: Record<Colors, string> = {
'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90', 'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
gray: 'bg-gray-300 dark:bg-gray-600', 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> </script>
<button <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 color
]}" ]} {hoverClasses}"
on:click on:click
> >
<slot /> <slot />

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,16 @@
import { getGithubVersion } from '$lib/utils/get-github-version'; import { getGithubVersion } from '$lib/utils/get-github-version';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import FullScreenModal from './full-screen-modal.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'; import Button from '../elements/buttons/button.svelte';
export let serverVersion: ServerVersionReponseDto; export let serverVersion: ServerVersionResponseDto;
let showModal = false; let showModal = false;
let githubVersion: string; let githubVersion: string;
$: serverVersionName = semverToName(serverVersion); $: serverVersionName = semverToName(serverVersion);
function semverToName({ major, minor, patch }: ServerVersionReponseDto) { function semverToName({ major, minor, patch }: ServerVersionResponseDto) {
return `v${major}.${minor}.${patch}`; 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"> <script lang="ts">
import '../app.css'; import '../app.css';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { afterNavigate, beforeNavigate } from '$app/navigation'; import { afterNavigate, beforeNavigate } from '$app/navigation';
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte'; 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 FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
import AppleHeader from '$lib/components/shared-components/apple-header.svelte'; import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
import FaviconHeader from '$lib/components/shared-components/favicon-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'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
let showNavigationLoadingBar = false; let showNavigationLoadingBar = false;
@ -29,6 +30,14 @@
showNavigationLoadingBar = false; showNavigationLoadingBar = false;
}); });
onMount(async () => {
try {
await loadFeatureFlags();
} catch (error) {
handleError(error, 'Unable to load feature flags');
}
});
const dropHandler = async ({ dataTransfer }: DragEvent) => { const dropHandler = async ({ dataTransfer }: DragEvent) => {
const files = dataTransfer?.files; const files = dataTransfer?.files;
if (!files) { if (!files) {