Compare commits
12 Commits
781599e2b7
...
c91973b185
Author | SHA1 | Date | |
---|---|---|---|
c91973b185 | |||
aca3eb457a | |||
99a77999fb | |||
d764e84726 | |||
f5c7ceb850 | |||
6fd78e7830 | |||
b0b41fcdc5 | |||
02f7c33b91 | |||
1468a9766d | |||
d6f51a5d1c | |||
7e7b8b6c8f | |||
06f6efe023 |
@ -49,7 +49,13 @@
|
|||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {catchError, mergeMap, Observable, retryWhen, timer} from "rxjs";
|
import {catchError, mergeMap, Observable, retryWhen, timer} from "rxjs";
|
||||||
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
|
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
|
||||||
import {NotifyColor, OpenNotifyService} from "@service/open-notify.service";
|
import {NotifyColor, OpenNotifyService} from "@service/open-notify.service";
|
||||||
import {environment} from "@/config/environment";
|
import {environment} from "@environment";
|
||||||
import {Router} from "@angular/router";
|
import {Router} from "@angular/router";
|
||||||
import {Injectable} from "@angular/core";
|
import {Injectable} from "@angular/core";
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import {DateOnly} from "@model/DateOnly";
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class SetupService extends ApiService {
|
export default class SetupService extends ApiService {
|
||||||
protected basePath = 'Setup/';
|
protected basePath = 'Setup/';
|
||||||
protected version = AvailableVersion.v1;
|
public readonly version = AvailableVersion.v1;
|
||||||
|
|
||||||
public checkToken(token: string) {
|
public checkToken(token: string) {
|
||||||
return this.get<boolean>('CheckToken', {token: token});
|
return this.get<boolean>('CheckToken', {token: token});
|
||||||
|
@ -1,7 +1,28 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {Routes} from '@angular/router';
|
|
||||||
import {ScheduleComponent} from "@page/schedule/schedule.component";
|
import {ScheduleComponent} from "@page/schedule/schedule.component";
|
||||||
|
import {WelcomeComponent} from "@page/setup/welcome/welcome.component";
|
||||||
|
import {DatabaseComponent} from "@page/setup/database/database.component";
|
||||||
|
import {CacheComponent} from "@page/setup/cache/cache.component";
|
||||||
|
import {LoggingComponent} from "@page/setup/logging/logging.component";
|
||||||
|
import {ScheduleComponent as SetupScheduleComponent} from "@page/setup/schedule/schedule.component";
|
||||||
|
import {SetupComponent} from "@page/setup/setup.component";
|
||||||
|
import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component";
|
||||||
|
import {SummaryComponent} from "@page/setup/summary/summary.component";
|
||||||
|
|
||||||
export const routes: Routes = [];
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
|
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
|
||||||
|
{
|
||||||
|
path: 'setup', title: 'Установка', component: SetupComponent, children: [
|
||||||
|
{path: 'welcome', component: WelcomeComponent},
|
||||||
|
{path: 'database', component: DatabaseComponent},
|
||||||
|
{path: 'cache', component: CacheComponent},
|
||||||
|
{path: 'create-admin', component: CreateAdminComponent},
|
||||||
|
{path: 'schedule', component: SetupScheduleComponent},
|
||||||
|
{path: 'logging', component: LoggingComponent},
|
||||||
|
{path: 'summary', component: SummaryComponent},
|
||||||
|
{path: '', redirectTo: 'welcome', pathMatch: 'full'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/*{path: 'not-found', title: '404 страница не найдена'},
|
||||||
|
{path: '**', redirectTo: '/not-found'}*/
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
apiUrl: 'http://localhost:5269/api/',
|
apiUrl: 'http://localhost:5269/api/',
|
||||||
production: false,
|
maxRetry: 5,
|
||||||
maxRetry: 3,
|
|
||||||
retryDelay: 1500
|
retryDelay: 1500
|
||||||
}
|
};
|
5
src/environments/environment.ts
Normal file
5
src/environments/environment.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const environment = {
|
||||||
|
apiUrl: '',
|
||||||
|
maxRetry: 3,
|
||||||
|
retryDelay: 1500
|
||||||
|
};
|
81
src/pages/setup/cache/cache.component.html
vendored
Normal file
81
src/pages/setup/cache/cache.component.html
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<h1>Настройка кэша</h1>
|
||||||
|
<hr/>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
На этой странице вы можете выбрать и настроить параметры кэширования для приложения.
|
||||||
|
</p>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
Укажите тип кэша, например, Redis, и настройте параметры подключения, чтобы улучшить производительность и снизить
|
||||||
|
нагрузку на базу данных.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form [formGroup]="databaseForm">
|
||||||
|
<p>
|
||||||
|
Выберите базу данных для хранения кэша:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>База данных</mat-label>
|
||||||
|
<mat-select (valueChange)="onDatabaseChange($event)">
|
||||||
|
<mat-option value="redis">Redis</mat-option>
|
||||||
|
<mat-option value="memcached">Memcached</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction: column;">
|
||||||
|
@if (database && database !== "memcached") {
|
||||||
|
<hr/>
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Сервер</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
||||||
|
required
|
||||||
|
formControlName="server">
|
||||||
|
|
||||||
|
@if (databaseForm.get('server')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Сервер является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('server')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Сервер должен содержать доменное имя сервера или ip адрес IPv4 или IPv6
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Порт</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите порт сервера"
|
||||||
|
required
|
||||||
|
formControlName="port">
|
||||||
|
|
||||||
|
@if (databaseForm.get('port')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Порт является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('port')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Порт должен содержать цифры НЕ начиная с цифры 0 и далее не менее 2 цифр
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Пароль</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите пароль"
|
||||||
|
formControlName="password"
|
||||||
|
[type]="hidePass ? 'password' : 'text'">
|
||||||
|
|
||||||
|
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
|
[attr.aria-pressed]="hidePass">
|
||||||
|
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</form>
|
69
src/pages/setup/cache/cache.component.ts
vendored
Normal file
69
src/pages/setup/cache/cache.component.ts
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cache',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInput,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon
|
||||||
|
],
|
||||||
|
templateUrl: './cache.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class CacheComponent {
|
||||||
|
protected databaseForm!: FormGroup;
|
||||||
|
protected database = '';
|
||||||
|
protected hidePass = true;
|
||||||
|
|
||||||
|
constructor(private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||||
|
this.databaseForm = this.formBuilder.group({
|
||||||
|
server: ['', Validators.pattern(/^([A-Za-z0-9]+\.)+[A-Za-z]{2,}$|^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^([A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$|^::1$/)],
|
||||||
|
port: ['', Validators.pattern(/^[1-9][0-9]{2,}$/)],
|
||||||
|
password: ['']
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.setNextButtonState(false);
|
||||||
|
this.databaseForm.valueChanges.subscribe(() => {
|
||||||
|
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDatabaseChange(selectedDatabase: string) {
|
||||||
|
this.database = selectedDatabase;
|
||||||
|
|
||||||
|
if (selectedDatabase === 'memcached') {
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.setMemcached();
|
||||||
|
};
|
||||||
|
this.navigationService.setNextButtonState(true);
|
||||||
|
} else {
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.setRedis({
|
||||||
|
"server": this.databaseForm.get('server')?.value,
|
||||||
|
"port": this.databaseForm.get('port')?.value,
|
||||||
|
"password": this.databaseForm.get('password')?.value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected togglePassword(event: MouseEvent) {
|
||||||
|
this.hidePass = !this.hidePass;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
109
src/pages/setup/create-admin/create-admin.component.html
Normal file
109
src/pages/setup/create-admin/create-admin.component.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<h1>Создание администратора</h1>
|
||||||
|
<hr/>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
На этой странице вы можете создать учетную запись администратора.
|
||||||
|
</p>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
Заполните необходимые поля, такие как имя пользователя, адрес электронной почты и пароль, чтобы создать учетную запись
|
||||||
|
с правами администратора для управления приложением.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form [formGroup]="createAdminForm">
|
||||||
|
<p>
|
||||||
|
Ведите данные для создания аккаунта администратора:
|
||||||
|
</p>
|
||||||
|
<div style="display:flex; flex-direction: column;">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Имя пользователя</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip='Укажите имя пользователя используя латинские буквы и цифры без пробелов'
|
||||||
|
required
|
||||||
|
formControlName="user">
|
||||||
|
|
||||||
|
@if (createAdminForm.get('user')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Имя пользователя является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (createAdminForm.get('user')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Имя пользователя должен содержать латинские сиволы и цифры и быть не менее 4 символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите email администратора"
|
||||||
|
required
|
||||||
|
formControlName="email">
|
||||||
|
|
||||||
|
@if (createAdminForm.get('email')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Email является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (createAdminForm.get('email')?.hasError('email')) {
|
||||||
|
<mat-error>
|
||||||
|
Введите корректный Email адрес
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent" style="margin-bottom: 20px">
|
||||||
|
<mat-label>Пароль</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите пароль"
|
||||||
|
formControlName="password"
|
||||||
|
required
|
||||||
|
[type]="hidePass ? 'password' : 'text'">
|
||||||
|
|
||||||
|
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
|
[attr.aria-pressed]="hidePass">
|
||||||
|
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (createAdminForm.get('password')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Пароль является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (createAdminForm.get('password')?.hasError('minlength')) {
|
||||||
|
<mat-error>
|
||||||
|
Пароль должен быть не менее 8 символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (createAdminForm.get('password')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!@#$%^&*)
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Повторите пароль</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите пароль, который был указан ранее"
|
||||||
|
formControlName="retype"
|
||||||
|
required
|
||||||
|
[type]="hideRetypePass ? 'password' : 'text'"
|
||||||
|
onpaste="return false;">
|
||||||
|
|
||||||
|
<button mat-icon-button matSuffix (click)="toggleRetypePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
|
[attr.aria-pressed]="hideRetypePass">
|
||||||
|
<mat-icon>{{ hideRetypePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (createAdminForm.get('retype')?.hasError('passwordsMismatch')) {
|
||||||
|
<mat-error>
|
||||||
|
Пароли не совпадают
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</form>
|
74
src/pages/setup/create-admin/create-admin.component.ts
Normal file
74
src/pages/setup/create-admin/create-admin.component.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import {passwordMatchValidator} from '@service/password-match.validator';
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-admin',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInput,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon
|
||||||
|
],
|
||||||
|
templateUrl: './create-admin.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class CreateAdminComponent {
|
||||||
|
protected createAdminForm!: FormGroup;
|
||||||
|
protected hidePass = true;
|
||||||
|
protected hideRetypePass = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||||
|
this.createAdminForm = this.formBuilder.group({
|
||||||
|
user: ['', Validators.pattern(/^([A-Za-z0-9]){4,}$/)],
|
||||||
|
email: ['', Validators.email],
|
||||||
|
password: ['', Validators.required],
|
||||||
|
retype: ['', Validators.required]
|
||||||
|
},
|
||||||
|
{validators: passwordMatchValidator('password', 'retype')}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.createAdminForm.get('password')?.setValidators([Validators.required,
|
||||||
|
Validators.pattern(/[A-Z]/),
|
||||||
|
Validators.pattern(/[!@#$%^&*]/),
|
||||||
|
Validators.minLength(8)
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.navigationService.setNextButtonState(false);
|
||||||
|
this.createAdminForm.valueChanges.subscribe(() => {
|
||||||
|
this.navigationService.setNextButtonState(this.createAdminForm.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.createAdmin({
|
||||||
|
"email": this.createAdminForm.get('email')?.value,
|
||||||
|
"username": this.createAdminForm.get('user')?.value,
|
||||||
|
"password": this.createAdminForm.get('password')?.value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected togglePassword(event: MouseEvent) {
|
||||||
|
this.hidePass = !this.hidePass;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toggleRetypePassword(event: MouseEvent) {
|
||||||
|
this.hideRetypePass = !this.hideRetypePass;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
156
src/pages/setup/database/database.component.html
Normal file
156
src/pages/setup/database/database.component.html
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<h1>Настройка базы данных</h1>
|
||||||
|
<hr/>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
На этой странице вы можете выбрать и настроить параметры подключения к базе данных.
|
||||||
|
</p>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
Укажите необходимую информацию, чтобы обеспечить правильное функционирование приложения.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mat-headline-6" style="color: red;font-weight: lighter;">
|
||||||
|
Данные настройки нельзя будет изменить в будущем!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form [formGroup]="databaseForm">
|
||||||
|
<p>
|
||||||
|
Выберите базу данных:
|
||||||
|
</p>
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>База данных</mat-label>
|
||||||
|
<mat-select (valueChange)="onDatabaseChange($event)">
|
||||||
|
<mat-option value="SetMysql">MySQL</mat-option>
|
||||||
|
<mat-option value="SetPsql">PostgreSQL</mat-option>
|
||||||
|
<mat-option value="sqlite">Sqlite</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (database) {
|
||||||
|
<hr/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction: column;">
|
||||||
|
@if (database === "sqlite") {
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Папка</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите папку, в которой будет находиться база данных"
|
||||||
|
formControlName="folder"
|
||||||
|
value="database"
|
||||||
|
required>
|
||||||
|
|
||||||
|
@if (databaseForm.get('folder')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Название папки является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('folder')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Название не может быть меньше 2 символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
} @else if (database) {
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Сервер</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
||||||
|
required
|
||||||
|
formControlName="server">
|
||||||
|
|
||||||
|
@if (databaseForm.get('server')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Сервер является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('server')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Сервер должен содержать доменное имя сервера или ip адрес IPv4 или IPv6
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Порт</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите порт сервера"
|
||||||
|
required
|
||||||
|
formControlName="port">
|
||||||
|
|
||||||
|
@if (databaseForm.get('port')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Порт является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('port')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Порт должен содержать цифры начиная НЕ с 0 и далее не менее 2 и не более 5 цифр
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Название базы данных</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите название базы данных"
|
||||||
|
required
|
||||||
|
formControlName="database_name">
|
||||||
|
|
||||||
|
@if (databaseForm.get('database_name')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Название базы данных является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('database_name')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Название не может быть меньше 2 символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Пользователь</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите пользователя, который имеет доступ к базе данных"
|
||||||
|
required
|
||||||
|
formControlName="user">
|
||||||
|
|
||||||
|
@if (databaseForm.get('user')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Имя пользователя базы данных является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (databaseForm.get('user')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Имя не может быть меньше 2 символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Пароль</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите пароль"
|
||||||
|
formControlName="password"
|
||||||
|
[type]="hidePass ? 'password' : 'text'">
|
||||||
|
|
||||||
|
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
|
[attr.aria-pressed]="hidePass">
|
||||||
|
|
||||||
|
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-checkbox
|
||||||
|
matTooltip="Использовать SSL/TLS для подключения к базе данных"
|
||||||
|
formControlName="ssl">
|
||||||
|
Использовать SSL/TLS
|
||||||
|
</mat-checkbox>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</form>
|
108
src/pages/setup/database/database.component.ts
Normal file
108
src/pages/setup/database/database.component.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {DatabaseRequest} from "@api/v1/databaseRequest";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatIcon} from "@angular/material/icon"
|
||||||
|
import {MatCheckbox} from "@angular/material/checkbox";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-database',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInput,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon,
|
||||||
|
MatCheckbox
|
||||||
|
],
|
||||||
|
templateUrl: './database.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class DatabaseComponent {
|
||||||
|
protected databaseForm!: FormGroup;
|
||||||
|
protected database = '';
|
||||||
|
protected hidePass = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||||
|
this.databaseForm = this.formBuilder.group({
|
||||||
|
folder: ['', Validators.pattern(/^.{2,}$/)],
|
||||||
|
server: ['', Validators.pattern(/^localhost$|^([A-Za-z0-9]+\.)+[A-Za-z]{2,}$|^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^([A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$|^::1$/)],
|
||||||
|
port: ['', Validators.pattern(/^[1-9][0-9]{1,4}$/)],
|
||||||
|
database_name: ['', Validators.pattern(/^.{2,}$/)],
|
||||||
|
user: ['', Validators.pattern(/^.{2,}$/)],
|
||||||
|
password: [''],
|
||||||
|
ssl: ['']
|
||||||
|
});
|
||||||
|
|
||||||
|
this.databaseForm.get('ssl')?.setValue(false);
|
||||||
|
this.navigationService.setNextButtonState(false);
|
||||||
|
this.databaseForm.valueChanges.subscribe(() => {
|
||||||
|
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createForm(database: string) {
|
||||||
|
if (database === 'sqlite') {
|
||||||
|
this.disableControls(['server', 'port', 'database_name', 'user', 'password']);
|
||||||
|
this.enableControls(['folder']);
|
||||||
|
} else {
|
||||||
|
this.enableControls(['server', 'port', 'database_name', 'user', 'password']);
|
||||||
|
this.disableControls(['folder']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableControls(controls: string[]) {
|
||||||
|
controls.forEach(control => {
|
||||||
|
this.databaseForm.get(control)!.disable({emitEvent: false});
|
||||||
|
this.databaseForm.get(control)!.updateValueAndValidity({emitEvent: false});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private enableControls(controls: string[]) {
|
||||||
|
controls.forEach(control => {
|
||||||
|
this.databaseForm.get(control)!.enable({emitEvent: false});
|
||||||
|
this.databaseForm.get(control)!.updateValueAndValidity({emitEvent: false});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDatabaseChange(selectedDatabase: string) {
|
||||||
|
this.createForm(selectedDatabase);
|
||||||
|
this.database = selectedDatabase;
|
||||||
|
|
||||||
|
if (this.database === "sqlite") {
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.setSqlite(this.databaseForm.get('folder')?.value ?? '');
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
let databaseRequest: DatabaseRequest = {
|
||||||
|
"server": this.databaseForm.get('server')?.value,
|
||||||
|
"port": this.databaseForm.get('port')?.value,
|
||||||
|
"database": this.databaseForm.get('database_name')?.value,
|
||||||
|
"user": this.databaseForm.get('user')?.value,
|
||||||
|
"ssl": this.databaseForm.get('ssl')?.value,
|
||||||
|
"password": this.databaseForm.get('password')?.value,
|
||||||
|
};
|
||||||
|
if (this.database === "mysql")
|
||||||
|
return this.api.setMysql(databaseRequest);
|
||||||
|
else
|
||||||
|
return this.api.setPsql(databaseRequest);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected togglePassword(event: MouseEvent) {
|
||||||
|
this.hidePass = !this.hidePass;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
39
src/pages/setup/logging/logging.component.html
Normal file
39
src/pages/setup/logging/logging.component.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<h1>Настройка логирования</h1>
|
||||||
|
<hr/>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
Настройте систему логирования как будет удобно для отображения.
|
||||||
|
Можно настроить путь к файлу, имена файлов или вовсе отключить логирование в файл.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form [formGroup]="loggingSettings">
|
||||||
|
<p>
|
||||||
|
Введите данные для настройки системы логирования:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction: column;">
|
||||||
|
<mat-checkbox
|
||||||
|
matTooltip='Использовать ли запись логов системы в файл'
|
||||||
|
formControlName="enabled"
|
||||||
|
(change)="isEnabledLoggingChange($event.checked)">
|
||||||
|
Включить логирование в файл
|
||||||
|
</mat-checkbox>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Путь к логам</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите путь к директории в зависимости от вашей системы"
|
||||||
|
formControlName="logPath">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Название файла</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите название файла, в который будут записаны логи"
|
||||||
|
formControlName="logName">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: center;">
|
||||||
|
<button mat-flat-button color="accent" (click)="skipButton()">Пропустить</button>
|
||||||
|
</div>
|
69
src/pages/setup/logging/logging.component.ts
Normal file
69
src/pages/setup/logging/logging.component.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatButton, MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatCheckbox} from "@angular/material/checkbox";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-logging',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInput,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatCheckbox,
|
||||||
|
MatButton
|
||||||
|
|
||||||
|
],
|
||||||
|
templateUrl: './logging.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class LoggingComponent {
|
||||||
|
protected loggingSettings!: FormGroup;
|
||||||
|
|
||||||
|
protected isEnabledLoggingChange(check: boolean) {
|
||||||
|
if (check) {
|
||||||
|
this.loggingSettings.get('logPath')?.enable();
|
||||||
|
this.loggingSettings.get('logName')?.enable();
|
||||||
|
} else {
|
||||||
|
this.loggingSettings.get('logPath')?.disable();
|
||||||
|
this.loggingSettings.get('logName')?.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected skipButton() {
|
||||||
|
this.navigationService.skipNavigation.emit(() => this.api.setLogging(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||||
|
this.loggingSettings = this.formBuilder.group({
|
||||||
|
enabled: [true, Validators.required],
|
||||||
|
logPath: [''],
|
||||||
|
logName: ['']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.navigationService.setNextButtonState(this.loggingSettings.valid);
|
||||||
|
this.loggingSettings.valueChanges.subscribe(() => {
|
||||||
|
this.navigationService.setNextButtonState(this.loggingSettings.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.setLogging({
|
||||||
|
"enableLogToFile": this.loggingSettings.get('cron')?.value,
|
||||||
|
"logFileName": this.loggingSettings.get('logName')?.value,
|
||||||
|
"logFilePath": this.loggingSettings.get('logPath')?.value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
56
src/pages/setup/schedule/schedule.component.html
Normal file
56
src/pages/setup/schedule/schedule.component.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<h1>Основных настройки расписания</h1>
|
||||||
|
<hr/>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
На этой странице вы можете настроить основные настройки для расписания.
|
||||||
|
</p>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
Заполните необходимые поля, чтобы настроить корректное отображение расписания.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form [formGroup]="scheduleSettings">
|
||||||
|
<p>
|
||||||
|
Ведите данные для настройки основных настроек расписания:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction: column;">
|
||||||
|
<p>
|
||||||
|
Чтобы заполнить Cron можно воспользоваться <a href="https://crontab.guru/" target="_blank">сторонними</a>
|
||||||
|
ресурсами для их генерации
|
||||||
|
</p>
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Укажите cron</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip='Расписание для автоматического обновление расписания вуза'
|
||||||
|
required
|
||||||
|
formControlName="cron">
|
||||||
|
|
||||||
|
@if (scheduleSettings.get('cron')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Cron является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (scheduleSettings.get('cron')?.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Cron должен быть формата: (* * * * *)
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Начало семестра</mat-label>
|
||||||
|
<input matInput [matDatepicker]="picker"
|
||||||
|
matTooltip="Укажите начало семестра"
|
||||||
|
required
|
||||||
|
formControlName="startTerm">
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #picker></mat-datepicker>
|
||||||
|
|
||||||
|
@if (scheduleSettings.get('startTerm')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Начало семестра является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</form>
|
56
src/pages/setup/schedule/schedule.component.ts
Normal file
56
src/pages/setup/schedule/schedule.component.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {DateAdapter, MatNativeDateModule} from "@angular/material/core";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-schedule-conf',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInput,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon,
|
||||||
|
MatDatepickerModule,
|
||||||
|
MatNativeDateModule
|
||||||
|
],
|
||||||
|
templateUrl: './schedule.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ScheduleComponent {
|
||||||
|
protected scheduleSettings!: FormGroup;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService, private _adapter: DateAdapter<any>) {
|
||||||
|
this._adapter.setLocale('ru');
|
||||||
|
this.scheduleSettings = this.formBuilder.group({
|
||||||
|
cron: ['0 */6 * * *', Validators.pattern(/^([^\s]+\s){4}[^\s]{1}$/)],
|
||||||
|
startTerm: ['', Validators.required]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.navigationService.setNextButtonState(false);
|
||||||
|
this.scheduleSettings.valueChanges.subscribe(() => {
|
||||||
|
this.navigationService.setNextButtonState(this.scheduleSettings.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.setSchedule({
|
||||||
|
"cronUpdateSchedule": this.scheduleSettings.get('cron')?.value,
|
||||||
|
"startTerm": this.scheduleSettings.get('startTerm')?.value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
32
src/pages/setup/setup.component.css
Normal file
32
src/pages/setup/setup.component.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.setup p {
|
||||||
|
letter-spacing: 0.55px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 26px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup .secondary {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup hr {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
display: flex;
|
||||||
|
padding: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-card {
|
||||||
|
padding: 15px 20px;
|
||||||
|
max-width: 750px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
32
src/pages/setup/setup.component.html
Normal file
32
src/pages/setup/setup.component.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<mat-sidenav-container>
|
||||||
|
<div class="setup">
|
||||||
|
<mat-card class="setup-card">
|
||||||
|
<router-outlet/>
|
||||||
|
<div class="setup-navigation">
|
||||||
|
<div>
|
||||||
|
<button mat-flat-button color="accent"
|
||||||
|
[disabled]="previousButtonDisabled"
|
||||||
|
[hidden]="previousButtonRoute === ''"
|
||||||
|
(click)="onPreviousClick()"
|
||||||
|
[routerLink]="previousButtonRoute">
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (loaderActive) {
|
||||||
|
<app-data-spinner [scale]="40"/>
|
||||||
|
} @else {
|
||||||
|
<button mat-flat-button color="accent"
|
||||||
|
[disabled]="nextButtonDisabled"
|
||||||
|
(click)="onNextClick()">
|
||||||
|
@if (getIndex === routes.length - 1) {
|
||||||
|
Завершить
|
||||||
|
} @else {
|
||||||
|
Далее
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</mat-sidenav-container>
|
101
src/pages/setup/setup.component.ts
Normal file
101
src/pages/setup/setup.component.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import {Component, ViewEncapsulation} from '@angular/core';
|
||||||
|
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||||
|
import {Router, RouterLink, RouterOutlet} from "@angular/router";
|
||||||
|
import {MatCard} from "@angular/material/card";
|
||||||
|
import {MatButton} from "@angular/material/button";
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import {catchError, Observable} from "rxjs";
|
||||||
|
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-setup',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatSidenavModule,
|
||||||
|
RouterOutlet,
|
||||||
|
MatCard,
|
||||||
|
MatButton,
|
||||||
|
RouterLink,
|
||||||
|
DataSpinnerComponent
|
||||||
|
],
|
||||||
|
templateUrl: './setup.component.html',
|
||||||
|
styleUrl: './setup.component.css',
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
providers: [SetupService]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class SetupComponent {
|
||||||
|
protected previousButtonDisabled: boolean = false;
|
||||||
|
protected previousButtonRoute: string = '';
|
||||||
|
|
||||||
|
protected nextButtonDisabled: boolean = false;
|
||||||
|
protected nextButtonRoute!: string;
|
||||||
|
|
||||||
|
protected loaderActive: boolean = false;
|
||||||
|
|
||||||
|
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'create-admin', 'schedule', 'logging', 'summary'];
|
||||||
|
private index: number = 1;
|
||||||
|
|
||||||
|
protected get getIndex() {
|
||||||
|
return this.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private router: Router, private navigationService: NavigationService, api: SetupService) {
|
||||||
|
api.isConfigured().subscribe(x => {
|
||||||
|
if (x)
|
||||||
|
this.router.navigate(['/']).then();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.router.url.includes(this.routes[this.index]))
|
||||||
|
this.router.navigate(['setup/', this.routes[this.index]]).then();
|
||||||
|
|
||||||
|
this.setRoutes();
|
||||||
|
this.navigationService.nextButtonState$.subscribe(state => {
|
||||||
|
this.nextButtonDisabled = !state;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.navigationService.skipNavigation.subscribe(action => {
|
||||||
|
this.executeAction(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setRoutes() {
|
||||||
|
this.previousButtonRoute = this.routes[this.index - 1];
|
||||||
|
this.nextButtonRoute = this.routes[this.index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeAction(action: () => Observable<boolean>) {
|
||||||
|
this.loaderActive = true;
|
||||||
|
action().pipe(
|
||||||
|
catchError(error => {
|
||||||
|
this.nextButtonDisabled = true;
|
||||||
|
this.loaderActive = false;
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(x => {
|
||||||
|
this.nextButtonDisabled = x;
|
||||||
|
this.loaderActive = !x;
|
||||||
|
if (x) {
|
||||||
|
if (this.index < this.routes.length - 1) {
|
||||||
|
this.router.navigate(['setup/', this.nextButtonRoute]).then();
|
||||||
|
this.index++;
|
||||||
|
this.setRoutes();
|
||||||
|
} else
|
||||||
|
this.router.navigate(['/']).then();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onNextClick() {
|
||||||
|
this.executeAction(this.navigationService.nextButtonAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onPreviousClick() {
|
||||||
|
if (this.index - 1 > 0) {
|
||||||
|
this.index--;
|
||||||
|
this.setRoutes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
src/pages/setup/welcome/welcome.component.html
Normal file
54
src/pages/setup/welcome/welcome.component.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<h1>Установщик MIREA Schedule</h1>
|
||||||
|
<hr/>
|
||||||
|
<p>
|
||||||
|
Мы рады помочь вам начать работу с нашим приложением.
|
||||||
|
Этот мастер настройки проведет вас через ряд шагов по настройке основных параметров вашей системы.
|
||||||
|
К концу этого процесса у вас будет все готово для использования приложения в полной мере.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Пожалуйста, внимательно следуйте инструкциям и предоставьте необходимую информацию при появлении запроса.
|
||||||
|
Если у вас есть какие-либо вопросы, обратитесь к документации или свяжитесь с нашей службой поддержки.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Для получения ключа используете тот же хост, что и Backend приложение и выполните:
|
||||||
|
</p>
|
||||||
|
<code>
|
||||||
|
curl -X 'GET' '{{apiToGetToken}}/Setup/GenerateToken' -H 'accept: application/json'
|
||||||
|
</code>
|
||||||
|
<p>
|
||||||
|
Или
|
||||||
|
</p>
|
||||||
|
<code>
|
||||||
|
Invoke-RestMethod -Uri "{{apiToGetToken}}/Setup/GenerateToken" -Method Get -Headers @{accept="application/json"}
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; margin: 25px 0;">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Токен сервера</mat-label>
|
||||||
|
<input matInput
|
||||||
|
required
|
||||||
|
[formControl]="tokenControl">
|
||||||
|
|
||||||
|
@if (tokenControl.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Токен является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (tokenControl.hasError('pattern')) {
|
||||||
|
<mat-error>
|
||||||
|
Токен должен содержать только BASE64 символы кратные 4 (цирфы, латинские буквы и символы "+/=")
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (tokenControl.hasError('minlength')) {
|
||||||
|
<mat-error>
|
||||||
|
Токен не может быть меньше 16 символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Для начала необходимо ввести токен, который нужен для настройки столь важных данных!</p>
|
45
src/pages/setup/welcome/welcome.component.ts
Normal file
45
src/pages/setup/welcome/welcome.component.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatButton} from "@angular/material/button";
|
||||||
|
import {NavigationService} from "@service/navigation.service";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {AsyncPipe} from "@angular/common";
|
||||||
|
import {FormControl, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {environment} from "@environment";
|
||||||
|
import {AvailableVersion} from "@api/api.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-welcome',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatButton,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInput,
|
||||||
|
AsyncPipe,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './welcome.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class WelcomeComponent {
|
||||||
|
protected tokenControl = new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}={2})$/),
|
||||||
|
Validators.minLength(16)
|
||||||
|
]);
|
||||||
|
|
||||||
|
protected apiToGetToken : string = environment.apiUrl;
|
||||||
|
|
||||||
|
constructor(private navigationService: NavigationService, private api: SetupService) {
|
||||||
|
this.apiToGetToken += AvailableVersion[this.api.version];
|
||||||
|
this.navigationService.nextButtonAction = () => {
|
||||||
|
return this.api.checkToken(this.tokenControl.value ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.navigationService.setNextButtonState(false);
|
||||||
|
this.tokenControl.valueChanges.subscribe(() => {
|
||||||
|
this.navigationService.setNextButtonState(this.tokenControl.valid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
18
src/services/navigation.service.ts
Normal file
18
src/services/navigation.service.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {EventEmitter, Injectable} from '@angular/core';
|
||||||
|
import {BehaviorSubject, Observable} from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class NavigationService {
|
||||||
|
private nextButtonState = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
nextButtonState$ = this.nextButtonState.asObservable();
|
||||||
|
nextButtonAction!: () => Observable<boolean>;
|
||||||
|
|
||||||
|
skipNavigation: EventEmitter<() => Observable<boolean>> = new EventEmitter();
|
||||||
|
|
||||||
|
setNextButtonState(state: boolean) {
|
||||||
|
this.nextButtonState.next(state);
|
||||||
|
}
|
||||||
|
}
|
17
src/services/password-match.validator.ts
Normal file
17
src/services/password-match.validator.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
|
||||||
|
|
||||||
|
export function passwordMatchValidator(password: string, retypePassword: string): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const passwordValue = control.get(password)?.value;
|
||||||
|
const retypePasswordControl = control.get(retypePassword);
|
||||||
|
|
||||||
|
if (retypePasswordControl?.value === passwordValue) {
|
||||||
|
retypePasswordControl!.setErrors(null);
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const error = {passwordsMismatch: true};
|
||||||
|
retypePasswordControl!.setErrors(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -37,6 +37,7 @@
|
|||||||
"./src/shared/requests/*",
|
"./src/shared/requests/*",
|
||||||
"./src/api/*"
|
"./src/api/*"
|
||||||
],
|
],
|
||||||
|
"@environment": ["./src/environments/environment.ts"],
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user