From 2b839088c738d1af79e494a7749d856deb0cfcd6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 18 Aug 2023 00:55:26 -0400 Subject: [PATCH] feat(web,server): server features (#3756) * feat: server features * chore: open api * icon size --------- Co-authored-by: Alex Tran --- cli/src/api/open-api/api.ts | 107 ++++++++++++++++-- cli/src/cli/base-command.ts | 4 +- .../models/server_info_state.model.dart | 4 +- .../providers/server_info.provider.dart | 4 +- .../shared/services/server_info.service.dart | 2 +- mobile/openapi/.openapi-generator/FILES | 9 +- mobile/openapi/README.md | Bin 18327 -> 18497 bytes mobile/openapi/doc/ServerFeaturesDto.md | Bin 0 -> 547 bytes mobile/openapi/doc/ServerInfoApi.md | Bin 6615 -> 7493 bytes ...onseDto.md => ServerVersionResponseDto.md} | Bin 468 -> 469 bytes mobile/openapi/lib/api.dart | Bin 5929 -> 5969 bytes mobile/openapi/lib/api/server_info_api.dart | Bin 7431 -> 8834 bytes mobile/openapi/lib/api_client.dart | Bin 18643 -> 18731 bytes .../lib/model/server_features_dto.dart | Bin 0 -> 3972 bytes ....dart => server_version_response_dto.dart} | Bin 3319 -> 3337 bytes .../test/server_features_dto_test.dart | Bin 0 -> 997 bytes mobile/openapi/test/server_info_api_test.dart | Bin 1096 -> 1224 bytes ... => server_version_response_dto_test.dart} | Bin 770 -> 773 bytes server/immich-openapi-specs.json | 52 ++++++++- server/src/domain/domain.constant.ts | 2 + server/src/domain/server-info/index.ts | 2 +- .../domain/server-info/response-dto/index.ts | 5 - .../response-dto/server-info-response.dto.ts | 19 ---- .../response-dto/server-ping-response.dto.ts | 10 -- .../response-dto/server-stats-response.dto.ts | 33 ------ .../server-version-response.dto.ts | 11 -- .../usage-by-user-response.dto.ts | 16 --- .../src/domain/server-info/server-info.dto.ts | 89 +++++++++++++++ .../server-info/server-info.service.spec.ts | 20 +++- .../domain/server-info/server-info.service.ts | 29 ++++- .../controllers/server-info.controller.ts | 19 +++- web/src/api/open-api/api.ts | 107 ++++++++++++++++-- .../admin-page/jobs/job-tile-button.svelte | 10 +- .../admin-page/jobs/job-tile.svelte | 12 +- .../admin-page/jobs/jobs-panel.svelte | 13 ++- .../navigation-bar/navigation-bar.svelte | 21 ++-- .../side-bar/side-bar.svelte | 9 +- .../version-announcement-box.svelte | 6 +- web/src/lib/stores/feature-flags.store.ts | 17 +++ web/src/routes/+layout.svelte | 13 ++- 40 files changed, 489 insertions(+), 156 deletions(-) create mode 100644 mobile/openapi/doc/ServerFeaturesDto.md rename mobile/openapi/doc/{ServerVersionReponseDto.md => ServerVersionResponseDto.md} (91%) create mode 100644 mobile/openapi/lib/model/server_features_dto.dart rename mobile/openapi/lib/model/{server_version_reponse_dto.dart => server_version_response_dto.dart} (63%) create mode 100644 mobile/openapi/test/server_features_dto_test.dart rename mobile/openapi/test/{server_version_reponse_dto_test.dart => server_version_response_dto_test.dart} (82%) delete mode 100644 server/src/domain/server-info/response-dto/index.ts delete mode 100644 server/src/domain/server-info/response-dto/server-info-response.dto.ts delete mode 100644 server/src/domain/server-info/response-dto/server-ping-response.dto.ts delete mode 100644 server/src/domain/server-info/response-dto/server-stats-response.dto.ts delete mode 100644 server/src/domain/server-info/response-dto/server-version-response.dto.ts delete mode 100644 server/src/domain/server-info/response-dto/usage-by-user-response.dto.ts create mode 100644 server/src/domain/server-info/server-info.dto.ts create mode 100644 web/src/lib/stores/feature-flags.store.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 03d260a6b..850aeae7f 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -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 => { + 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> { + 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> { + async getServerVersion(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { 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 { + 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 { + getServerVersion(options?: AxiosRequestConfig): AxiosPromise { 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. diff --git a/cli/src/cli/base-command.ts b/cli/src/cli/base-command.ts index 06247e29c..c2fb8fee9 100644 --- a/cli/src/cli/base-command.ts +++ b/cli/src/cli/base-command.ts @@ -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; diff --git a/mobile/lib/shared/models/server_info_state.model.dart b/mobile/lib/shared/models/server_info_state.model.dart index 7b1ea9c91..6623ac166 100644 --- a/mobile/lib/shared/models/server_info_state.model.dart +++ b/mobile/lib/shared/models/server_info_state.model.dart @@ -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, }) { diff --git a/mobile/lib/shared/providers/server_info.provider.dart b/mobile/lib/shared/providers/server_info.provider.dart index 2613b8522..ff0945ead 100644 --- a/mobile/lib/shared/providers/server_info.provider.dart +++ b/mobile/lib/shared/providers/server_info.provider.dart @@ -10,7 +10,7 @@ class ServerInfoNotifier extends StateNotifier { ServerInfoNotifier(this._serverInfoService) : super( ServerInfoState( - serverVersion: ServerVersionReponseDto( + serverVersion: ServerVersionResponseDto( major: 0, patch_: 0, minor: 0, @@ -23,7 +23,7 @@ class ServerInfoNotifier extends StateNotifier { final ServerInfoService _serverInfoService; getServerVersion() async { - ServerVersionReponseDto? serverVersion = + ServerVersionResponseDto? serverVersion = await _serverInfoService.getServerVersion(); if (serverVersion == null) { diff --git a/mobile/lib/shared/services/server_info.service.dart b/mobile/lib/shared/services/server_info.service.dart index b112fc46c..bf923dfab 100644 --- a/mobile/lib/shared/services/server_info.service.dart +++ b/mobile/lib/shared/services/server_info.service.dart @@ -24,7 +24,7 @@ class ServerInfoService { } } - Future getServerVersion() async { + Future getServerVersion() async { try { return await _apiService.serverInfoApi.getServerVersion(); } catch (e) { diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index d8e4b1fad..c51b8a3e0 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 042d69591f174f50b0b5fa91b48a6f9532d95961..6c6108a9fe07fe912a9b475d93c1e6561f0eb271 100644 GIT binary patch delta 117 zcmbQ<&vb*;g(FoPq{a(1X+ z9xNBhHW6l5i8?uLeQbI*7{mq!l<)H4&82IJq4CDCAsf+#I5P616N^VE&(C3@)BsIq zELI5K8rv!rHA+1a_0uz=NFo@y`=eEkOcji_`Gw>lgje#qqSn~cJ|TYKv|Q|V*D*`U zB-bf^*_*)o{Y6nM?pEb;y($KsX==s+EknvCe8T~Cf|Cz0*-jSV3g>{Snw-ZK54W*fFkJyA0o1Og6{~@&9VobY yqJ$gcYXi~tw51Lgn# diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e83c7b868bf3b748646bfcf7b0932cfffa654151..064a265175fd91984af5d7b743e2c5eb3560162f 100644 GIT binary patch delta 41 xcmZ3fcTsPHt^h|`YGO%gQEKsILw>2r>jhXR>k90fY{n-yIZBXcvxuN58vrr54DSE{ delta 20 ccmcbpw^DC|uE69Ffqj!F3Tke)5fo(u093jL@&Et; diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart index fb34e09ce866e5939102add22182e894f575b904..3b7899cc264fde989628cfd36cd903aa35d92bfc 100644 GIT binary patch delta 163 zcmZp-YI5Chg^^u9Ej6*Ev}m%vkRx|$QCVt{8-(8=l*9p%N-f@8C$XDR79x>a>{60% zZ>Nx+S`v(`{)&js$W6W|7`piYkGlW>8k9RK delta 34 qcmZp2ZMWKRg>kbF`xExfhWyi*CL4&0PQEM1IXRvuWV5x9y8r;;>kF&^ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a46580899d5c50f488c6e540f9acd45a0ab70b54..830aab93695e865c257d2824ab67b50c662a68de 100644 GIT binary patch delta 72 zcmcaSk#Y4T#tret>~5)vC8b5FlLO@?_<~c5%2JDS`SkYu3@$!io(FKYgzM!kTrJN&Ui|e6 z*@*MQnrR!o4S#w&p`+MJrFpzjI^KwkpFklC!`Cs-_>M~(RA;fvmA0c2EZeh#iSxq7 zn*UP>jpCB+@NdpE{Tsv_y`dB6;H*u!q7TIjdWc>4w zags7^CKG(lg4%$}xn#Kr@%P(a=lIv=nk1jIbzH*!0PZ)55AtR>)+OB}b3; zO0O@~R>(0O1|RmKkH;7SfP1Af=vmcVI9qzh#)9Ue^KL`-uetLFg;Ca+$?8qag=SW1 zh@#o@{tHhqLpJ657@`IEj3+FcQPhI!=G!;_3B*|5UcYwH^ep{I<1>9Xp)25gB{u(I zvOv>6870wh zU+x~$p7z|B-l5c3qv(k_FB@ie9Ajx5?lCtPJ+V$48leuk zC*ot&@KzZ1-H&>x^OmF@spb5)-U>{v!4DY&!W#I>vVt4fXtgVqtsDv4@dg>D)Ua2W zs=EMWk!8Le?1gb%x>Yc_<{KOpZp!E)!B%8qYEm%{9V?O;=ToZIR6w3E?C`%hBV77R zIC$eKETBCzXBzb@w5Xy3bWq}0!h{(+z{|`x=~g5Mr7=ST{Z9`|C)n@uKPgzt&FvxQ z@2S-v+Ia(H@1-a~L8)d1@y@(uU2t*L{yXY_Px%2$7RSU`sK3&dsz7Cz_?ZrEC%l(`xUg}GMte3#opr4+%2Lv~i`R(o7p9BXr5!O=rC z8sa>pT66xO>1BKO+;rkWPY|${os!cj*P`beclP*h_d^+L9IaPb&yUw#5k{b*|+T63x-cgIdiEQbl5aM8mi^)Ijo5|{u0 literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/server_version_reponse_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart similarity index 63% rename from mobile/openapi/lib/model/server_version_reponse_dto.dart rename to mobile/openapi/lib/model/server_version_response_dto.dart index a1ec0e68e5208667dc703ba95f5f034655ec0d88..353d65049b25e067219cfb2b5254e616abdeac5d 100644 GIT binary patch delta 117 zcmew^*(tTbjFGW;vKga3kW6J%1(IEiuNjLsmoljVB{t7yRsqTHXITv-vsk-<)!Ttz@qgZmDUWa0S?03;PD A0{{R3 delta 81 zcmV-X0IvUu8uuBnHUX120WXth0VR`-0qe7Q0ww{opaUcUlf?w9lW_%$ll=wklj;U! nlTQbIlg$UglaB~alimo@ld%cHlXMDElg0{ilQ|3AlNJo~8fhVh diff --git a/mobile/openapi/test/server_features_dto_test.dart b/mobile/openapi/test/server_features_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d2ae364c4b70ff5bb600baf475a66f11f99a8334 GIT binary patch literal 997 zcmbV}-%H~_5Xay1SBy`sP>tu4D8d!eiqJEl^}(lgOs2_Vy1UNqDn!07MIhzJ;8eNca0p*j}Lzx zXZ@%fW0};#vRdelP9d7;saDj{l1vzV&U)!Ahpijfz2M3#7lUGYikfBJ;8IS9Ebl<< zMV$Ti^~xF_&;+GxG1Lo9oYwtTvrZfGs5@V dvyC?2w)%_yxtW(s+M(1M+f7~~ryF|9-T}+tIdcF2 literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart index 4fee682a2552353d272a2b126ba67cdbd30acc6f..8ca3e30ef1979221ac931e937941aa8e3b49114c 100644 GIT binary patch delta 66 zcmX@Xae{NhJw`dV)Wnj~qSRuSl6*Ub^wg5z)S|M~BADppKa7eTaE>CA#pHX8EsVvR JlbD_{0svxw8CL)R delta 16 YcmX@Xd4glZJ;uqgOf8!qF+F1h06ta+B>(^b diff --git a/mobile/openapi/test/server_version_reponse_dto_test.dart b/mobile/openapi/test/server_version_response_dto_test.dart similarity index 82% rename from mobile/openapi/test/server_version_reponse_dto_test.dart rename to mobile/openapi/test/server_version_response_dto_test.dart index 3095e7a4629873c6da15e0c5fba4e9bb0b0389d0..add42ccd66713cf6651c877df0867ce91a2789a7 100644 GIT binary patch delta 26 gcmZo-Yh~Mz!^l`XIfu~(NUmViVl3W#iP3-&0Bnc{<^TWy delta 20 bcmZo=Yhv4w!#FvY(Pr{0My<_P84VZ#M}Y>A diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 4078e687d..91f72c170 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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" diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index 4c881d7ee..7b60b796a 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -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'; diff --git a/server/src/domain/server-info/index.ts b/server/src/domain/server-info/index.ts index 72113665a..74a46a52b 100644 --- a/server/src/domain/server-info/index.ts +++ b/server/src/domain/server-info/index.ts @@ -1,2 +1,2 @@ -export * from './response-dto'; +export * from './server-info.dto'; export * from './server-info.service'; diff --git a/server/src/domain/server-info/response-dto/index.ts b/server/src/domain/server-info/response-dto/index.ts deleted file mode 100644 index 47cbd2ff8..000000000 --- a/server/src/domain/server-info/response-dto/index.ts +++ /dev/null @@ -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'; diff --git a/server/src/domain/server-info/response-dto/server-info-response.dto.ts b/server/src/domain/server-info/response-dto/server-info-response.dto.ts deleted file mode 100644 index e844da689..000000000 --- a/server/src/domain/server-info/response-dto/server-info-response.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/domain/server-info/response-dto/server-ping-response.dto.ts b/server/src/domain/server-info/response-dto/server-ping-response.dto.ts deleted file mode 100644 index 8b41b4af1..000000000 --- a/server/src/domain/server-info/response-dto/server-ping-response.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/domain/server-info/response-dto/server-stats-response.dto.ts b/server/src/domain/server-info/response-dto/server-stats-response.dto.ts deleted file mode 100644 index 1459ba452..000000000 --- a/server/src/domain/server-info/response-dto/server-stats-response.dto.ts +++ /dev/null @@ -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[]; -} diff --git a/server/src/domain/server-info/response-dto/server-version-response.dto.ts b/server/src/domain/server-info/response-dto/server-version-response.dto.ts deleted file mode 100644 index 373fa734f..000000000 --- a/server/src/domain/server-info/response-dto/server-version-response.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/domain/server-info/response-dto/usage-by-user-response.dto.ts b/server/src/domain/server-info/response-dto/usage-by-user-response.dto.ts deleted file mode 100644 index ac3a82907..000000000 --- a/server/src/domain/server-info/response-dto/usage-by-user-response.dto.ts +++ /dev/null @@ -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; -} diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts new file mode 100644 index 000000000..ea0699aa6 --- /dev/null +++ b/server/src/domain/server-info/server-info.dto.ts @@ -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; +} diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index ebb0d800f..764e1c889 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -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; let storageMock: jest.Mocked; let userMock: jest.Mocked; 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', () => { diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index 4a7e7f22b..e628d12ba 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -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 { 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 { + 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 { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); diff --git a/server/src/immich/controllers/server-info.controller.ts b/server/src/immich/controllers/server-info.controller.ts index 59b635178..3b69c3532 100644 --- a/server/src/immich/controllers/server-info.controller.ts +++ b/server/src/immich/controllers/server-info.controller.ts @@ -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 { + return this.service.getFeatures(); + } + @AdminRoute() - @Get('/stats') + @Get('stats') getStats(): Promise { return this.service.getStats(); } @PublicRoute() - @Get('/media-types') + @Get('media-types') getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 03d260a6b..850aeae7f 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -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 => { + 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> { + 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> { + async getServerVersion(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { 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 { + 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 { + getServerVersion(options?: AxiosRequestConfig): AxiosPromise { 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. diff --git a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte index b794c387c..709ed6092 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte @@ -4,17 +4,23 @@ - {#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]}
- - -
- -
-
-
+ {#if $featureFlags.search} + + +
+ +
+
+
+ {/if} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 0abf17481..770bbfa23 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -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 @@ - - - + {#if $featureFlags.search} + + + + {/if} diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte index a59ab0e40..af9856828 100644 --- a/web/src/lib/components/shared-components/version-announcement-box.svelte +++ b/web/src/lib/components/shared-components/version-announcement-box.svelte @@ -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}`; } diff --git a/web/src/lib/stores/feature-flags.store.ts b/web/src/lib/stores/feature-flags.store.ts new file mode 100644 index 000000000..119ecd557 --- /dev/null +++ b/web/src/lib/stores/feature-flags.store.ts @@ -0,0 +1,17 @@ +import { api, ServerFeaturesDto } from '@api'; +import { writable } from 'svelte/store'; + +export type FeatureFlags = ServerFeaturesDto; + +export const featureFlags = writable({ + machineLearning: true, + search: true, + oauth: true, + oauthAutoLaunch: true, + passwordLogin: true, +}); + +export const loadFeatureFlags = async () => { + const { data } = await api.serverInfoApi.getServerFeatures(); + featureFlags.update(() => data); +}; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index d224461bb..c57c04f51 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -1,6 +1,5 @@