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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 698 additions and 111 deletions

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 };
}