feat(web): onboarding (#6066)

* feat(web): onboarding

* feat: openapi

* feat: modulization

* feat: page advancing

* Animation

* Add storage templaete settings

* sql

* more style

* Theme

* information and styling

* hide/show table

* Styling

* Update user property

* fix test

* fix test:

* fix e2e

* test

* Update web/src/lib/components/onboarding-page/onboarding-hello.svelte

Co-authored-by: bo0tzz <git@bo0tzz.me>

* naming

* use System Metadata

* better return type

* onboarding using server metadata

* revert previous changes in user entity

* sql

* test web

* fix test server

* server/web test

* more test

* consolidate color theme change logic

* consolidate save button to storage template

* merge main

* fix web

---------

Co-authored-by: bo0tzz <git@bo0tzz.me>
This commit is contained in:
Alex
2024-01-03 23:28:32 -06:00
committed by GitHub
parent f8d64be13c
commit 18f59f78e3
35 changed files with 698 additions and 111 deletions

View File

@@ -4725,6 +4725,31 @@
]
}
},
"/server-info/admin-onboarding": {
"post": {
"operationId": "setAdminOnboarding",
"parameters": [],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Server Info"
]
}
},
"/server-info/config": {
"get": {
"operationId": "getServerConfig",
@@ -8599,6 +8624,9 @@
"isInitialized": {
"type": "boolean"
},
"isOnboarded": {
"type": "boolean"
},
"loginPageMessage": {
"type": "string"
},
@@ -8614,6 +8642,7 @@
"oauthButtonText",
"loginPageMessage",
"isInitialized",
"isOnboarded",
"externalDomain"
],
"type": "object"

View File

@@ -86,6 +86,7 @@ export class ServerConfigDto {
@ApiProperty({ type: 'integer' })
trashDays!: number;
isInitialized!: boolean;
isOnboarded!: boolean;
externalDomain!: string;
}

View File

@@ -1,8 +1,10 @@
import { SystemMetadataKey } from '@app/infra/entities';
import {
newCommunicationRepositoryMock,
newServerInfoRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
newSystemMetadataRepositoryMock,
newUserRepositoryMock,
} from '@test';
import { serverVersion } from '../domain.constant';
@@ -11,6 +13,7 @@ import {
IServerInfoRepository,
IStorageRepository,
ISystemConfigRepository,
ISystemMetadataRepository,
IUserRepository,
} from '../repositories';
import { ServerInfoService } from './server-info.service';
@@ -22,6 +25,7 @@ describe(ServerInfoService.name, () => {
let serverInfoMock: jest.Mocked<IServerInfoRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
let systemMetadataMock: jest.Mocked<ISystemMetadataRepository>;
beforeEach(() => {
configMock = newSystemConfigRepositoryMock();
@@ -29,8 +33,16 @@ describe(ServerInfoService.name, () => {
serverInfoMock = newServerInfoRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
systemMetadataMock = newSystemMetadataRepositoryMock();
sut = new ServerInfoService(communicationMock, configMock, userMock, serverInfoMock, storageMock);
sut = new ServerInfoService(
communicationMock,
configMock,
userMock,
serverInfoMock,
storageMock,
systemMetadataMock,
);
});
it('should work', () => {
@@ -184,12 +196,21 @@ describe(ServerInfoService.name, () => {
loginPageMessage: '',
oauthButtonText: 'Login with OAuth',
trashDays: 30,
isInitialized: undefined,
isOnboarded: false,
externalDomain: '',
});
expect(configMock.load).toHaveBeenCalled();
});
});
describe('setAdminOnboarding', () => {
it('should set admin onboarding to true', async () => {
await sut.setAdminOnboarding();
expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
});
});
describe('getStats', () => {
it('should total up usage by user', async () => {
userMock.getUserStats.mockResolvedValue([

View File

@@ -1,3 +1,4 @@
import { SystemMetadataKey } from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger';
import { Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
@@ -9,6 +10,7 @@ import {
IServerInfoRepository,
IStorageRepository,
ISystemConfigRepository,
ISystemMetadataRepository,
IUserRepository,
UserStatsQueryResponse,
} from '../repositories';
@@ -37,6 +39,7 @@ export class ServerInfoService {
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) private readonly systemMetadataRepository: ISystemMetadataRepository,
) {
this.configCore = SystemConfigCore.create(configRepository);
this.communicationRepository.on('connect', (userId) => this.handleConnect(userId));
@@ -79,16 +82,22 @@ export class ServerInfoService {
async getConfig(): Promise<ServerConfigDto> {
const config = await this.configCore.getConfig();
const isInitialized = await this.userRepository.hasAdmin();
const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
return {
loginPageMessage: config.server.loginPageMessage,
trashDays: config.trash.days,
oauthButtonText: config.oauth.buttonText,
isInitialized,
isOnboarded: onboarding?.isOnboarded || false,
externalDomain: config.server.externalDomain,
};
}
setAdminOnboarding(): Promise<void> {
return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
}
async getStatistics(): Promise<ServerStatsResponseDto> {
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
const serverStats = new ServerStatsResponseDto();

View File

@@ -9,7 +9,7 @@ import {
ServerThemeDto,
ServerVersionResponseDto,
} from '@app/domain';
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AdminRoute, Authenticated, PublicRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
@@ -67,4 +67,11 @@ export class ServerInfoController {
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return this.service.getSupportedMediaTypes();
}
@AdminRoute()
@Post('admin-onboarding')
@HttpCode(HttpStatus.NO_CONTENT)
setAdminOnboarding(): Promise<void> {
return this.service.setAdminOnboarding();
}
}

View File

@@ -11,8 +11,10 @@ export class SystemMetadataEntity {
export enum SystemMetadataKey {
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
ADMIN_ONBOARDING = 'admin-onboarding',
}
export interface SystemMetadata extends Record<SystemMetadataKey, { [key: string]: unknown }> {
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
}

View File

@@ -5,6 +5,7 @@ import { assetApi } from './asset-api';
import { authApi } from './auth-api';
import { libraryApi } from './library-api';
import { partnerApi } from './partner-api';
import { serverInfoApi } from './server-info-api';
import { sharedLinkApi } from './shared-link-api';
import { userApi } from './user-api';
@@ -14,6 +15,7 @@ export const api = {
apiKeyApi,
assetApi,
libraryApi,
serverInfoApi,
sharedLinkApi,
albumApi,
userApi,

View File

@@ -0,0 +1,10 @@
import { ServerConfigDto } from '@app/domain';
import request from 'supertest';
export const serverInfoApi = {
getConfig: async (server: any) => {
const res = await request(server).get('/server-info/config');
expect(res.status).toBe(200);
return res.body as ServerConfigDto;
},
};

View File

@@ -98,6 +98,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
trashDays: 30,
isInitialized: true,
externalDomain: '',
isOnboarded: false,
});
});
});
@@ -167,4 +168,19 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
});
});
});
describe('POST /server-info/admin-onboarding', () => {
it('should set admin onboarding', async () => {
const config = await api.serverInfoApi.getConfig(server);
expect(config.isOnboarded).toBe(false);
const { status } = await request(server)
.post('/server-info/admin-onboarding')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const newConfig = await api.serverInfoApi.getConfig(server);
expect(newConfig.isOnboarded).toBe(true);
});
});
});

View File

@@ -19,6 +19,7 @@ export * from './smart-info.repository.mock';
export * from './storage.repository.mock';
export * from './system-config.repository.mock';
export * from './system-info.repository.mock';
export * from './system-metadata.repository.mock';
export * from './tag.repository.mock';
export * from './user-token.repository.mock';
export * from './user.repository.mock';

View File

@@ -0,0 +1,8 @@
import { ISystemMetadataRepository } from '@app/domain';
export const newSystemMetadataRepositoryMock = (): jest.Mocked<ISystemMetadataRepository> => {
return {
get: jest.fn(),
set: jest.fn(),
};
};