ended form mode interface
This commit is contained in:
348
ui/src/app/knock/FORM_ARRAY_DOCUMENTATION.md
Normal file
348
ui/src/app/knock/FORM_ARRAY_DOCUMENTATION.md
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
# Документация: FormArray в компоненте KnockPageComponent
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
В компоненте `KnockPageComponent` используется Angular FormArray для управления динамическими формами целей (targets) в режиме "form". Это позволяет пользователям добавлять, удалять и редактировать неограниченное количество целей для пропинывания портов.
|
||||||
|
|
||||||
|
## Архитектура FormArray
|
||||||
|
|
||||||
|
### 1. Структура данных
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
form = this.fb.group({
|
||||||
|
// ... другие поля
|
||||||
|
targetForms: this.fb.array([]) // FormArray для динамических форм
|
||||||
|
});
|
||||||
|
|
||||||
|
get targetForms(): FormArray {
|
||||||
|
return this.form.get('targetForms') as FormArray;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Структура отдельной формы цели
|
||||||
|
|
||||||
|
Каждая форма цели содержит следующие поля:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private createTargetForm(): FormGroup {
|
||||||
|
return this.fb.group({
|
||||||
|
protocol: ['tcp', Validators.required], // Протокол (TCP/UDP)
|
||||||
|
host: ['127.0.0.1', Validators.required], // IP адрес хоста
|
||||||
|
port: [22, [Validators.required, Validators.min(1), Validators.max(65535)]], // Порт
|
||||||
|
gateway: [''] // Шлюз (опционально)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Основные методы работы с FormArray
|
||||||
|
|
||||||
|
### 1. Создание новой формы цели
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
addTarget(): void {
|
||||||
|
const newTargetForm = this.createTargetForm();
|
||||||
|
this.targetForms.push(newTargetForm);
|
||||||
|
this.serializeFormTargets();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что происходит:**
|
||||||
|
|
||||||
|
- Создается новая FormGroup с полями по умолчанию
|
||||||
|
- Форма добавляется в FormArray через `push()`
|
||||||
|
- Автоматически вызывается сериализация данных
|
||||||
|
|
||||||
|
### 2. Удаление формы цели
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
removeTarget(index: number): void {
|
||||||
|
if (this.targetForms.length > 1) {
|
||||||
|
this.targetForms.removeAt(index);
|
||||||
|
this.serializeFormTargets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
|
||||||
|
- Защита от удаления последней формы (минимум 1 форма)
|
||||||
|
- Используется `removeAt(index)` для удаления по индексу
|
||||||
|
- Автоматическая сериализация после удаления
|
||||||
|
|
||||||
|
### 3. Сериализация данных форм
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private serializeFormTargets(): void {
|
||||||
|
if (this.form.value.mode !== 'form') return;
|
||||||
|
|
||||||
|
const targets: string[] = [];
|
||||||
|
this.targetForms.controls.forEach(targetForm => {
|
||||||
|
const value = targetForm.value;
|
||||||
|
if (value.protocol && value.host && value.port) {
|
||||||
|
let targetString = `${value.protocol}:${value.host}:${value.port}`;
|
||||||
|
if (value.gateway && value.gateway.trim()) {
|
||||||
|
targetString += `:${value.gateway.trim()}`;
|
||||||
|
}
|
||||||
|
targets.push(targetString);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.patchValue({ targets: targets.join(';') });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Процесс сериализации:**
|
||||||
|
|
||||||
|
1. Проверка, что текущий режим - "form"
|
||||||
|
2. Итерация по всем формам в FormArray
|
||||||
|
3. Сборка строки в формате `protocol:host:port:gateway`
|
||||||
|
4. Объединение всех целей через `;`
|
||||||
|
5. Обновление поля `targets` в основной форме
|
||||||
|
|
||||||
|
## Интеграция с HTML шаблоном
|
||||||
|
|
||||||
|
### 1. Отображение форм
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="form-targets-list">
|
||||||
|
<div
|
||||||
|
*ngFor="let targetForm of targetForms.controls; let i = index"
|
||||||
|
class="form-target-item"
|
||||||
|
[formGroup]="$any(targetForm)"
|
||||||
|
>
|
||||||
|
<!-- Поля формы -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ключевые моменты:**
|
||||||
|
|
||||||
|
- `*ngFor` итерируется по `targetForms.controls`
|
||||||
|
- `[formGroup]` связывает каждую форму с FormGroup
|
||||||
|
- `$any(targetForm)` решает проблему типизации TypeScript
|
||||||
|
|
||||||
|
### 2. Поля формы
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<label>Protocol</label>
|
||||||
|
<p-dropdown
|
||||||
|
formControlName="protocol"
|
||||||
|
[options]="[
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' }
|
||||||
|
]"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
></p-dropdown>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Автоматическое сохранение и восстановление
|
||||||
|
|
||||||
|
### 1. Подписка на изменения
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private setupAutoSave() {
|
||||||
|
// Подписка на изменения в формах целей
|
||||||
|
this.targetForms.valueChanges.subscribe(() => {
|
||||||
|
if (this.form.value.mode === 'form') {
|
||||||
|
this.serializeFormTargets();
|
||||||
|
setTimeout(() => this.saveStateToLocalStorage(), 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Сохранение в localStorage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private saveStateToLocalStorage() {
|
||||||
|
const state: any = {
|
||||||
|
// ... другие поля
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сохраняем данные форм целей для режима form
|
||||||
|
if (formValue.mode === 'form' && this.targetForms.length > 0) {
|
||||||
|
state.targetForms = this.targetForms.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Восстановление из localStorage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private loadStateFromLocalStorage() {
|
||||||
|
// ... загрузка других полей
|
||||||
|
|
||||||
|
// Загружаем сохраненные формы целей для режима form
|
||||||
|
if (state.mode === 'form' && state.targetForms && Array.isArray(state.targetForms)) {
|
||||||
|
this.targetForms.clear();
|
||||||
|
state.targetForms.forEach((targetData: any) => {
|
||||||
|
const targetForm = this.fb.group({
|
||||||
|
protocol: [targetData.protocol || 'tcp', Validators.required],
|
||||||
|
host: [targetData.host || '127.0.0.1', Validators.required],
|
||||||
|
port: [targetData.port || 22, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: [targetData.gateway || '']
|
||||||
|
});
|
||||||
|
this.targetForms.push(targetForm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Преобразование между режимами
|
||||||
|
|
||||||
|
### 1. Конвертация в режим Form
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private convertInlineToForm() {
|
||||||
|
const targetsString = this.form.value.targets || '';
|
||||||
|
const targets = targetsString.split(';').filter(t => t.trim());
|
||||||
|
|
||||||
|
targets.forEach(target => {
|
||||||
|
const parts = target.trim().split(':');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const targetForm = this.fb.group({
|
||||||
|
protocol: [parts[0] || 'tcp', Validators.required],
|
||||||
|
host: [parts[1] || '127.0.0.1', Validators.required],
|
||||||
|
port: [parseInt(parts[2]) || 22, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: [parts[3] || '']
|
||||||
|
});
|
||||||
|
this.targetForms.push(targetForm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Конвертация из режима Form
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private handleModeChangeFromForm(previousMode: string, newMode: string) {
|
||||||
|
// Сначала сериализуем данные формы
|
||||||
|
this.serializeFormTargets();
|
||||||
|
|
||||||
|
if (newMode === 'inline') {
|
||||||
|
// Данные уже в targets, ничего дополнительно не нужно
|
||||||
|
} else if (newMode === 'yaml') {
|
||||||
|
this.convertInlineToYaml();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Жизненный цикл FormArray
|
||||||
|
|
||||||
|
### 1. Инициализация
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private initializeFormMode(): void {
|
||||||
|
// Если нет форм целей, создаем одну по умолчанию
|
||||||
|
if (this.targetForms.length === 0) {
|
||||||
|
this.addTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Очистка при смене режима
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private handleModeChangeToForm(previousMode: string) {
|
||||||
|
// Очищаем существующие формы
|
||||||
|
this.targetForms.clear();
|
||||||
|
|
||||||
|
// Конвертируем данные из предыдущего режима
|
||||||
|
if (previousMode === 'inline') {
|
||||||
|
this.convertInlineToForm();
|
||||||
|
} else if (previousMode === 'yaml') {
|
||||||
|
this.convertYamlToForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем формы если их нет
|
||||||
|
if (this.targetForms.length === 0) {
|
||||||
|
this.addTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Валидация
|
||||||
|
|
||||||
|
### 1. Валидация полей формы
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private createTargetForm(): FormGroup {
|
||||||
|
return this.fb.group({
|
||||||
|
protocol: ['tcp', Validators.required],
|
||||||
|
host: ['127.0.0.1', Validators.required],
|
||||||
|
port: [22, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Защита от удаления всех форм
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
removeTarget(index: number): void {
|
||||||
|
if (this.targetForms.length > 1) {
|
||||||
|
this.targetForms.removeAt(index);
|
||||||
|
this.serializeFormTargets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Преимущества использования FormArray
|
||||||
|
|
||||||
|
1. **Динамичность**: Возможность добавления/удаления форм в runtime
|
||||||
|
2. **Валидация**: Встроенная валидация для каждой формы
|
||||||
|
3. **Реактивность**: Автоматическое обновление UI при изменениях
|
||||||
|
4. **Типобезопасность**: TypeScript поддержка
|
||||||
|
5. **Интеграция**: Легкая интеграция с Angular Reactive Forms
|
||||||
|
6. **Сериализация**: Простое преобразование в различные форматы
|
||||||
|
|
||||||
|
## Потенциальные проблемы и решения
|
||||||
|
|
||||||
|
### 1. Проблема типизации в шаблоне
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
```html
|
||||||
|
[formGroup]="targetForm" <!-- TypeScript ошибка -->
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```html
|
||||||
|
[formGroup]="$any(targetForm)" <!-- Приведение типа -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Защита от пустого FormArray
|
||||||
|
|
||||||
|
**Проблема:** Пользователь может удалить все формы
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```typescript
|
||||||
|
removeTarget(index: number): void {
|
||||||
|
if (this.targetForms.length > 1) { // Минимум 1 форма
|
||||||
|
this.targetForms.removeAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Производительность при большом количестве форм
|
||||||
|
|
||||||
|
**Проблема:** Много форм может замедлить приложение
|
||||||
|
|
||||||
|
**Решение:** Виртуализация или пагинация (не реализовано в текущей версии)
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
FormArray в данном компоненте обеспечивает гибкую и мощную систему управления динамическими формами. Реализация включает:
|
||||||
|
|
||||||
|
- Полный жизненный цикл форм (создание, редактирование, удаление)
|
||||||
|
- Автоматическую сериализацию/десериализацию
|
||||||
|
- Интеграцию с системой режимов
|
||||||
|
- Сохранение состояния в localStorage
|
||||||
|
- Валидацию и защиту от некорректных данных
|
||||||
|
|
||||||
|
Этот подход делает компонент удобным для пользователей и легко расширяемым для разработчиков.
|
@@ -71,7 +71,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-4" *ngIf="form.value.mode === 'inline'">
|
<div class="col-12 md:col-4" *ngIf="form.value.mode !== 'yaml'">
|
||||||
<label>Delay</label>
|
<label>Delay</label>
|
||||||
<input
|
<input
|
||||||
pInputText
|
pInputText
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
<label class="checkbox-label">Wait connection</label>
|
<label class="checkbox-label">Wait connection</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12" style="display: none;">
|
||||||
<label>Gateway</label>
|
<label>Gateway</label>
|
||||||
<input
|
<input
|
||||||
pInputText
|
pInputText
|
||||||
@@ -117,6 +117,93 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Mode Section -->
|
||||||
|
<div class="col-12" *ngIf="form.value.mode === 'form'">
|
||||||
|
<div class="form-targets-container">
|
||||||
|
<label>Targets Configuration</label>
|
||||||
|
<div class="form-targets-list">
|
||||||
|
<div
|
||||||
|
*ngFor="let targetForm of targetForms.controls; let i = index"
|
||||||
|
class="form-target-item"
|
||||||
|
[formGroup]="$any(targetForm)"
|
||||||
|
>
|
||||||
|
<div class="form-target-header">
|
||||||
|
<h4>Target {{ i + 1 }}</h4>
|
||||||
|
<button
|
||||||
|
*ngIf="targetForms.length > 1"
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
class="p-button-danger p-button-sm"
|
||||||
|
(click)="removeTarget(i)"
|
||||||
|
pTooltip="Remove target"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<label>Protocol</label>
|
||||||
|
<p-dropdown
|
||||||
|
formControlName="protocol"
|
||||||
|
[options]="[
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' }
|
||||||
|
]"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
></p-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<label>Host</label>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
type="text"
|
||||||
|
formControlName="host"
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<label>Port</label>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
type="number"
|
||||||
|
formControlName="port"
|
||||||
|
placeholder="22"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<label>Gateway</label>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
type="text"
|
||||||
|
formControlName="gateway"
|
||||||
|
placeholder="optional"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-target-actions">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="Add Target"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
class="p-button-outlined"
|
||||||
|
(click)="addTarget()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File controls directly under YAML -->
|
<!-- File controls directly under YAML -->
|
||||||
<div class="col-12" *ngIf="form.value.mode === 'yaml'">
|
<div class="col-12" *ngIf="form.value.mode === 'yaml'">
|
||||||
<div class="flex flex-wrap gap-2 align-items-center">
|
<div class="flex flex-wrap gap-2 align-items-center">
|
||||||
@@ -171,7 +258,7 @@
|
|||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 2: Encrypt / Decrypt half/half on desktop; stacked on mobile (только для YAML режима) -->
|
<!-- Row 2: Encrypt / Decrypt half/half on desktop; stacked on mobile (только для YAML режима) -->
|
||||||
<div class="col-12 md:col-6" *ngIf="!isInlineMode()">
|
<div class="col-12 md:col-6" *ngIf="form.value.mode === 'yaml'">
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -186,7 +273,7 @@
|
|||||||
"
|
"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6" *ngIf="!isInlineMode()">
|
<div class="col-12 md:col-6" *ngIf="form.value.mode === 'yaml'">
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -202,7 +289,7 @@
|
|||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 3: Download actions half/half on desktop; stacked on mobile (только для YAML режима) -->
|
<!-- Row 3: Download actions half/half on desktop; stacked on mobile (только для YAML режима) -->
|
||||||
<div class="col-12 md:col-6" *ngIf="!isInlineMode()">
|
<div class="col-12 md:col-6" *ngIf="form.value.mode === 'yaml'">
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -211,7 +298,7 @@
|
|||||||
class="p-button-text w-full"
|
class="p-button-text w-full"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6" *ngIf="!isInlineMode()">
|
<div class="col-12 md:col-6" *ngIf="form.value.mode === 'yaml'">
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
type="button"
|
type="button"
|
||||||
|
@@ -130,3 +130,46 @@ input[type='text'], input[type='password'], select, textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form Mode Styles
|
||||||
|
.form-targets-container {
|
||||||
|
.form-targets-list {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-target-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-target-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-target-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component, inject, Input } from '@angular/core';
|
import { Component, inject, Input } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators, FormsModule } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators, FormsModule, FormArray, FormGroup } from '@angular/forms';
|
||||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { PasswordModule } from 'primeng/password';
|
import { PasswordModule } from 'primeng/password';
|
||||||
@@ -14,6 +14,7 @@ import { DividerModule } from 'primeng/divider';
|
|||||||
import { FileUploadModule } from 'primeng/fileupload';
|
import { FileUploadModule } from 'primeng/fileupload';
|
||||||
import { ProgressBarModule } from 'primeng/progressbar';
|
import { ProgressBarModule } from 'primeng/progressbar';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { DialogModule } from 'primeng/dialog';
|
||||||
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ import { environment } from '../../environments/environment';
|
|||||||
CommonModule, RouterModule, ReactiveFormsModule, FormsModule,
|
CommonModule, RouterModule, ReactiveFormsModule, FormsModule,
|
||||||
InputTextModule, PasswordModule, DropdownModule, CheckboxModule,
|
InputTextModule, PasswordModule, DropdownModule, CheckboxModule,
|
||||||
InputTextareaModule, ButtonModule, CardModule, DividerModule,
|
InputTextareaModule, ButtonModule, CardModule, DividerModule,
|
||||||
FileUploadModule, ProgressBarModule, DialogModule
|
FileUploadModule, ProgressBarModule, DialogModule, TooltipModule
|
||||||
],
|
],
|
||||||
templateUrl: './knock-page.component.html',
|
templateUrl: './knock-page.component.html',
|
||||||
styleUrls: ['./knock-page.component.scss']
|
styleUrls: ['./knock-page.component.scss']
|
||||||
@@ -64,9 +65,14 @@ export class KnockPageComponent {
|
|||||||
waitConnection: [false],
|
waitConnection: [false],
|
||||||
gateway: [''],
|
gateway: [''],
|
||||||
configYAML: [''],
|
configYAML: [''],
|
||||||
serverFilePath: ['']
|
serverFilePath: [''],
|
||||||
|
targetForms: this.fb.array([])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get targetForms(): FormArray {
|
||||||
|
return this.form.get('targetForms') as FormArray;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Загружаем сохраненное состояние из localStorage
|
// Загружаем сохраненное состояние из localStorage
|
||||||
this.loadStateFromLocalStorage();
|
this.loadStateFromLocalStorage();
|
||||||
@@ -108,6 +114,58 @@ export class KnockPageComponent {
|
|||||||
|
|
||||||
// File System Access API detection (для обратной совместимости)
|
// File System Access API detection (для обратной совместимости)
|
||||||
// Логика FSA теперь находится в отдельных компонентах
|
// Логика FSA теперь находится в отдельных компонентах
|
||||||
|
|
||||||
|
// Инициализируем с одной формой по умолчанию для режима form
|
||||||
|
this.initializeFormMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form Mode Methods
|
||||||
|
private createTargetForm(): FormGroup {
|
||||||
|
return this.fb.group({
|
||||||
|
protocol: ['tcp', Validators.required],
|
||||||
|
host: ['127.0.0.1', Validators.required],
|
||||||
|
port: [22, [Validators.required, Validators.min(1), Validators.max(65_535)]],
|
||||||
|
gateway: ['']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addTarget(): void {
|
||||||
|
const newTargetForm = this.createTargetForm();
|
||||||
|
this.targetForms.push(newTargetForm);
|
||||||
|
this.serializeFormTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTarget(index: number): void {
|
||||||
|
if (this.targetForms.length > 1) {
|
||||||
|
this.targetForms.removeAt(index);
|
||||||
|
this.serializeFormTargets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeFormMode(): void {
|
||||||
|
// Если нет форм целей, создаем одну по умолчанию
|
||||||
|
if (this.targetForms.length === 0) {
|
||||||
|
this.addTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeFormTargets(): void {
|
||||||
|
if (this.form.value.mode !== 'form') return;
|
||||||
|
|
||||||
|
const targets: string[] = [];
|
||||||
|
this.targetForms.controls.forEach(targetForm => {
|
||||||
|
const value = targetForm.value;
|
||||||
|
if (!(value.protocol && value.host && value.port)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let targetString = `${value.protocol}:${value.host}:${value.port}`;
|
||||||
|
if (value.gateway?.trim()) {
|
||||||
|
targetString += `:${value.gateway.trim()}`;
|
||||||
|
}
|
||||||
|
targets.push(targetString);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.patchValue({ targets: targets.join(';') });
|
||||||
}
|
}
|
||||||
|
|
||||||
private authHeader(pass: string) {
|
private authHeader(pass: string) {
|
||||||
@@ -466,6 +524,21 @@ export class KnockPageComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загружаем сохраненные формы целей для режима form
|
||||||
|
if (state.mode === 'form' && state.targetForms && Array.isArray(state.targetForms)) {
|
||||||
|
this.targetForms.clear();
|
||||||
|
state.targetForms.forEach((targetData: any) => {
|
||||||
|
const targetForm = this.fb.group({
|
||||||
|
protocol: [targetData.protocol || 'tcp', Validators.required],
|
||||||
|
host: [targetData.host || '127.0.0.1', Validators.required],
|
||||||
|
port: [targetData.port || 22, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: [targetData.gateway || '']
|
||||||
|
});
|
||||||
|
this.targetForms.push(targetForm);
|
||||||
|
});
|
||||||
|
environment.log('Loaded form targets from localStorage:', state.targetForms);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load state from localStorage:', e);
|
console.warn('Failed to load state from localStorage:', e);
|
||||||
}
|
}
|
||||||
@@ -474,7 +547,7 @@ export class KnockPageComponent {
|
|||||||
private saveStateToLocalStorage() {
|
private saveStateToLocalStorage() {
|
||||||
try {
|
try {
|
||||||
const formValue = this.form.value;
|
const formValue = this.form.value;
|
||||||
const state = {
|
const state: any = {
|
||||||
mode: formValue.mode,
|
mode: formValue.mode,
|
||||||
targets: formValue.targets,
|
targets: formValue.targets,
|
||||||
delay: formValue.delay,
|
delay: formValue.delay,
|
||||||
@@ -483,6 +556,12 @@ export class KnockPageComponent {
|
|||||||
configYAML: formValue.configYAML
|
configYAML: formValue.configYAML
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Сохраняем данные форм целей для режима form
|
||||||
|
if (formValue.mode === 'form' && this.targetForms.length > 0) {
|
||||||
|
state.targetForms = this.targetForms.value;
|
||||||
|
environment.log('Saving form targets to localStorage:', state.targetForms);
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
||||||
environment.log('State saved to localStorage:', state);
|
environment.log('State saved to localStorage:', state);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -500,6 +579,14 @@ export class KnockPageComponent {
|
|||||||
setTimeout(() => this.saveStateToLocalStorage(), 300);
|
setTimeout(() => this.saveStateToLocalStorage(), 300);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Подписываемся на изменения в формах целей для режима form
|
||||||
|
this.targetForms.valueChanges.subscribe(() => {
|
||||||
|
if (this.form.value.mode === 'form') {
|
||||||
|
this.serializeFormTargets();
|
||||||
|
setTimeout(() => this.saveStateToLocalStorage(), 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Автоматическое преобразование между режимами
|
// Автоматическое преобразование между режимами
|
||||||
@@ -515,6 +602,10 @@ export class KnockPageComponent {
|
|||||||
this.handleModeChangeToYaml();
|
this.handleModeChangeToYaml();
|
||||||
} else if (previousMode === 'yaml' && newMode === 'inline') {
|
} else if (previousMode === 'yaml' && newMode === 'inline') {
|
||||||
this.handleModeChangeToInline();
|
this.handleModeChangeToInline();
|
||||||
|
} else if (previousMode === 'form' && (newMode === 'inline' || newMode === 'yaml')) {
|
||||||
|
this.handleModeChangeFromForm(previousMode, newMode);
|
||||||
|
} else if ((previousMode === 'inline' || previousMode === 'yaml') && newMode === 'form') {
|
||||||
|
this.handleModeChangeToForm(previousMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousMode = newMode;
|
previousMode = newMode;
|
||||||
@@ -699,6 +790,130 @@ export class KnockPageComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form Mode Conversion Methods
|
||||||
|
private handleModeChangeToForm(previousMode: string) {
|
||||||
|
try {
|
||||||
|
environment.log(`Converting from ${previousMode} to form mode`);
|
||||||
|
|
||||||
|
// Очищаем существующие формы
|
||||||
|
this.targetForms.clear();
|
||||||
|
|
||||||
|
if (previousMode === 'inline') {
|
||||||
|
this.convertInlineToForm();
|
||||||
|
} else if (previousMode === 'yaml') {
|
||||||
|
this.convertYamlToForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем формы если их нет
|
||||||
|
if (this.targetForms.length === 0) {
|
||||||
|
this.addTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to convert to form mode:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleModeChangeFromForm(previousMode: string, newMode: string) {
|
||||||
|
try {
|
||||||
|
environment.log(`Converting from form mode to ${newMode}`);
|
||||||
|
|
||||||
|
// Сначала сериализуем данные формы
|
||||||
|
this.serializeFormTargets();
|
||||||
|
|
||||||
|
if (newMode === 'inline') {
|
||||||
|
// Данные уже в targets, ничего дополнительно не нужно
|
||||||
|
environment.log('Form data serialized to inline targets');
|
||||||
|
} else if (newMode === 'yaml') {
|
||||||
|
this.convertInlineToYaml();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to convert from form mode:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertInlineToForm() {
|
||||||
|
try {
|
||||||
|
const targetsString = this.form.value.targets || '';
|
||||||
|
if (!targetsString.trim()) {
|
||||||
|
this.addTarget();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = targetsString.split(';').filter(t => t.trim());
|
||||||
|
|
||||||
|
targets.forEach(target => {
|
||||||
|
const parts = target.trim().split(':');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const protocol = parts[0] || 'tcp';
|
||||||
|
const host = parts[1] || '127.0.0.1';
|
||||||
|
const port = parseInt(parts[2]) || 22;
|
||||||
|
const gateway = parts[3] || '';
|
||||||
|
|
||||||
|
const targetForm = this.fb.group({
|
||||||
|
protocol: [protocol, Validators.required],
|
||||||
|
host: [host, Validators.required],
|
||||||
|
port: [port, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: [gateway]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.targetForms.push(targetForm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
environment.log('Converted inline to form:', targets);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to convert inline to form:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertYamlToForm() {
|
||||||
|
try {
|
||||||
|
const yamlContent = this.form.value.configYAML || '';
|
||||||
|
if (!yamlContent.trim()) {
|
||||||
|
this.addTarget();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: any = yaml.load(yamlContent);
|
||||||
|
if (!config || !config.targets || !Array.isArray(config.targets)) {
|
||||||
|
this.addTarget();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.targets.forEach((target: any) => {
|
||||||
|
const protocol = target.protocol || 'tcp';
|
||||||
|
const host = target.host || '127.0.0.1';
|
||||||
|
const ports = target.ports || [target.port] || [22];
|
||||||
|
|
||||||
|
if (Array.isArray(ports)) {
|
||||||
|
ports.forEach((port: number) => {
|
||||||
|
const targetForm = this.fb.group({
|
||||||
|
protocol: [protocol, Validators.required],
|
||||||
|
host: [host, Validators.required],
|
||||||
|
port: [port, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: ['']
|
||||||
|
});
|
||||||
|
this.targetForms.push(targetForm);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const targetForm = this.fb.group({
|
||||||
|
protocol: [protocol, Validators.required],
|
||||||
|
host: [host, Validators.required],
|
||||||
|
port: [ports, [Validators.required, Validators.min(1), Validators.max(65535)]],
|
||||||
|
gateway: ['']
|
||||||
|
});
|
||||||
|
this.targetForms.push(targetForm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
environment.log('Converted YAML to form:', config.targets);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to convert YAML to form:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Публичный метод для очистки сохраненного состояния (опционально)
|
// Публичный метод для очистки сохраненного состояния (опционально)
|
||||||
clearSavedState() {
|
clearSavedState() {
|
||||||
try {
|
try {
|
||||||
|
Reference in New Issue
Block a user