Files
FemaInstaller/internal/ui/main_window.go

379 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ui
import (
"fmt"
"path/filepath"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding" // <-- ИМПОРТИРУЕМ BINDING
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"gitea.unprism.ru/KRBL/FemaDeployer/internal/deployer"
"gitea.unprism.ru/KRBL/FemaDeployer/pkg/config"
)
const configFileName = "deployer_config.json"
// AppUI представляет главный UI приложения
type AppUI struct {
app fyne.App
window fyne.Window
// Общие данные
connConfig *config.ConnectionConfig
deployer *deployer.Deployer
manifest *config.Manifest
statuses []deployer.ServiceStatus // Статусы пока оставляем как slice, они обновляются редко
// Виджеты
ipEntry *widget.Entry
portEntry *widget.Entry
loginEntry *widget.Entry
passwordEntry *widget.Entry
packagePathLabel *widget.Label
statusList *widget.List
componentList *widget.List
latestLogLabel *widget.Label // Метка для отображения последнего лога
logData binding.StringList // Хранилище всех логов
// Состояние для выбора компонентов
selectedComponents map[string]bool
}
// NewAppUI создает новый UI
func NewAppUI(app fyne.App) *AppUI {
w := app.NewWindow("Fema Deployer")
ui := &AppUI{
app: app,
window: w,
selectedComponents: make(map[string]bool),
logData: binding.NewStringList(), // <-- ИНИЦИАЛИЗИРУЕМ BINDING
}
// Загружаем конфиг
ui.connConfig, _ = config.Load(configFileName)
// Инициализируем Deployer с функцией логирования
ui.deployer = deployer.NewDeployer(ui.connConfig, ui.addLog)
// Больше не нужен таймер для прокрутки логов, так как мы показываем только последний лог
// Создаем вкладки
tabs := container.NewAppTabs(
container.NewTabItem("Статус", ui.createStatusTab()),
container.NewTabItem("Развертывание", ui.createDeployTab()),
)
// Создаем лог-панель
logPanel := ui.createLogPanel()
// Собираем главный контейнер
mainContent := container.NewBorder(tabs, logPanel, nil, nil, nil)
w.SetContent(mainContent)
w.Resize(fyne.NewSize(800, 600))
return ui
}
func (ui *AppUI) createStatusTab() fyne.CanvasObject {
// ... код полей для подключения без изменений ...
ui.ipEntry = widget.NewEntry()
ui.ipEntry.SetText(ui.connConfig.IP)
ui.portEntry = widget.NewEntry()
ui.portEntry.SetText(ui.connConfig.Port)
ui.loginEntry = widget.NewEntry()
ui.loginEntry.SetText(ui.connConfig.Login)
ui.passwordEntry = widget.NewPasswordEntry()
ui.passwordEntry.SetText(ui.connConfig.Password)
connectForm := widget.NewForm(
widget.NewFormItem("IP", ui.ipEntry),
widget.NewFormItem("Port", ui.portEntry),
widget.NewFormItem("Login", ui.loginEntry),
widget.NewFormItem("Password", ui.passwordEntry),
)
// Список статусов
ui.statusList = widget.NewList(
func() int {
return len(ui.statuses)
},
func() fyne.CanvasObject {
return container.NewGridWithColumns(3, widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel(""))
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
if id >= len(ui.statuses) {
return
}
s := ui.statuses[id]
c := obj.(*fyne.Container)
c.Objects[0].(*widget.Label).SetText(s.Name)
c.Objects[1].(*widget.Label).SetText(s.Version)
c.Objects[2].(*widget.Label).SetText(s.Status)
},
)
// Кнопка обновления статуса
refreshBtn := widget.NewButton("Проверить статус", ui.onRefreshStatus)
return container.NewBorder(
container.NewVBox(connectForm, refreshBtn),
nil, nil, nil,
container.NewMax(ui.statusList),
)
}
func (ui *AppUI) createDeployTab() fyne.CanvasObject {
ui.packagePathLabel = widget.NewLabel("Пакет развертывания не выбран")
if ui.connConfig.DeploymentPackagePath != "" {
ui.packagePathLabel.SetText(filepath.Base(ui.connConfig.DeploymentPackagePath))
go ui.loadManifest() // Загружаем манифест при старте, если путь уже есть
}
selectPackageBtn := widget.NewButton("Выбрать пакет (.tar.gz)", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil || reader == nil {
return
}
defer reader.Close()
path := reader.URI().Path()
if path == "" {
return
}
// Эти операции быстрые, их оставляем в основном потоке
ui.connConfig.DeploymentPackagePath = path
ui.packagePathLabel.SetText(filepath.Base(path))
// А вот долгую операцию запускаем в отдельной горутине
go ui.loadManifest()
}, ui.window)
})
// ... остальной код функции createDeployTab без изменений ...
ui.componentList = widget.NewList(
func() int {
if ui.manifest == nil {
return 0
}
return len(ui.manifest.Components)
},
func() fyne.CanvasObject {
return container.NewHBox(widget.NewCheck("", func(b bool) {}), widget.NewLabel(""))
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
comp := ui.manifest.Components[id]
c := obj.(*fyne.Container)
check := c.Objects[0].(*widget.Check)
label := c.Objects[1].(*widget.Label)
label.SetText(fmt.Sprintf("%s (v%s)", comp.Name, comp.Version))
check.SetText("")
check.Checked = ui.selectedComponents[comp.Name]
check.OnChanged = func(b bool) {
ui.selectedComponents[comp.Name] = b
}
},
)
fullInstallBtn := widget.NewButton("Полная установка", ui.onFullDeploy)
selectiveUpdateBtn := widget.NewButton("Обновить выбранное", ui.onSelectiveUpdate)
return container.NewVBox(
container.NewHBox(selectPackageBtn, ui.packagePathLabel),
widget.NewSeparator(),
container.NewMax(ui.componentList),
widget.NewSeparator(),
container.NewGridWithColumns(2, fullInstallBtn, selectiveUpdateBtn),
)
}
func (ui *AppUI) createLogPanel() fyne.CanvasObject {
// Создаем метку для отображения последнего лога
ui.latestLogLabel = widget.NewLabel("Готов к работе")
// Кнопка для просмотра всех логов
viewLogsBtn := widget.NewButton("Просмотр всех логов", func() {
ui.showLogsWindow()
})
// Размещаем метку и кнопку в контейнере
return container.NewBorder(nil, nil, nil, viewLogsBtn, ui.latestLogLabel)
}
// showLogsWindow открывает новое окно со всеми логами
func (ui *AppUI) showLogsWindow() {
logWindow := ui.app.NewWindow("Журнал операций")
// Создаем многострочное текстовое поле для отображения всех логов
logEntry := widget.NewEntry()
logEntry.MultiLine = true
logEntry.Wrapping = fyne.TextWrapWord
logEntry.Disable() // Делаем поле только для чтения, но с возможностью копирования
// Получаем все логи и объединяем их в одну строку
count := ui.logData.Length()
var allLogs strings.Builder
for i := 0; i < count; i++ {
item, err := ui.logData.GetItem(i)
if err != nil {
continue
}
s, err := item.(binding.String).Get()
if err != nil {
continue
}
allLogs.WriteString(s)
allLogs.WriteString("\n")
}
// Устанавливаем текст в поле
logEntry.SetText(allLogs.String())
// Кнопка закрытия
closeBtn := widget.NewButton("Закрыть", func() {
logWindow.Close()
})
// Размещаем текстовое поле и кнопку в контейнере
content := container.NewBorder(nil, closeBtn, nil, nil, container.NewScroll(logEntry))
logWindow.SetContent(content)
logWindow.Resize(fyne.NewSize(600, 400))
logWindow.Show()
}
// --- Обработчики событий ---
func (ui *AppUI) onRefreshStatus() {
ui.updateAndSaveConfig()
if ui.manifest == nil {
ui.addLog("Ошибка: сначала выберите пакет развертывания на вкладке 'Развертывание'.")
dialog.ShowInformation("Ошибка", "Сначала выберите пакет развертывания", ui.window)
return
}
go func() {
statuses, err := ui.deployer.GetServicesStatus(ui.manifest.Components)
if err != nil {
ui.addLog(fmt.Sprintf("Ошибка проверки статуса: %v", err))
return
}
// Безопасно обновляем данные и виджет
ui.statuses = statuses
ui.statusList.Refresh() // Fyne спроектирован так, что Refresh можно вызывать из других горутин
}()
}
// ... функции onFullDeploy и onSelectiveUpdate без изменений ...
func (ui *AppUI) onFullDeploy() {
ui.updateAndSaveConfig()
if ui.connConfig.DeploymentPackagePath == "" {
dialog.ShowInformation("Ошибка", "Пакет развертывания не выбран", ui.window)
return
}
dialog.ShowConfirm("Подтверждение", "Это сотрет предыдущие установки и выполнит установку с нуля. Продолжить?", func(b bool) {
if !b {
return
}
go func() {
err := ui.deployer.FullDeploy()
if err != nil {
ui.addLog(fmt.Sprintf("Ошибка полной установки: %v", err))
}
}()
}, ui.window)
}
func (ui *AppUI) onSelectiveUpdate() {
ui.updateAndSaveConfig()
var toUpdate []string
for name, selected := range ui.selectedComponents {
if selected {
toUpdate = append(toUpdate, name)
}
}
if len(toUpdate) == 0 {
dialog.ShowInformation("Информация", "Не выбраны компоненты для обновления", ui.window)
return
}
go func() {
err := ui.deployer.SelectiveUpdate(toUpdate)
if err != nil {
ui.addLog(fmt.Sprintf("Ошибка выборочного обновления: %v", err))
}
}()
}
// --- Вспомогательные функции ---
func (ui *AppUI) updateAndSaveConfig() {
// Мьютекс здесь не нужен, т.к. доступ к виджетам идет из основного потока
ui.connConfig.IP = ui.ipEntry.Text
ui.connConfig.Port = ui.portEntry.Text
ui.connConfig.Login = ui.loginEntry.Text
ui.connConfig.Password = ui.passwordEntry.Text
// Путь к пакету уже обновлен, сохраняем все вместе
ui.connConfig.Save(configFileName)
}
func (ui *AppUI) loadManifest() {
ui.addLog("Загрузка манифеста...")
loadedPackage, err := deployer.LoadDeploymentPackage(ui.connConfig.DeploymentPackagePath)
if err != nil {
ui.addLog(fmt.Sprintf("Ошибка загрузки манифеста: %v", err))
// Можно показать диалог ошибки, но делать это нужно в основном потоке.
// Для этого можно использовать канал или просто положиться на лог.
return
}
ui.manifest = loadedPackage.Manifest
ui.addLog("Манифест успешно загружен. Компоненты:")
for _, c := range ui.manifest.Components {
ui.addLog(fmt.Sprintf("- %s (v%s)", c.Name, c.Version))
}
ui.componentList.Refresh()
// Сразу после успешной загрузки манифеста запускаем проверку статусов.
// Нет необходимости делать это через ui.onRefreshStatus, можно напрямую.
go func() {
statuses, err := ui.deployer.GetServicesStatus(ui.manifest.Components)
if err != nil {
ui.addLog(fmt.Sprintf("Ошибка первоначальной проверки статуса: %v", err))
return
}
ui.statuses = statuses
ui.statusList.Refresh()
}()
}
func (ui *AppUI) addLog(message string) {
// Форматируем сообщение с временной меткой
logLine := fmt.Sprintf("%s: %s", time.Now().Format("15:04:05"), message)
// Добавляем в историю логов (потокобезопасно через binding)
ui.logData.Append(logLine)
// Обновляем метку с последним логом
// Fyne поддерживает обновление виджетов из любого потока
if ui.latestLogLabel != nil {
fyne.Do(func() {
if !strings.Contains(ui.latestLogLabel.Text, "\n") {
ui.latestLogLabel.SetText(logLine)
}
})
}
}
// ShowAndRun показывает окно и запускает приложение
func (ui *AppUI) ShowAndRun() {
ui.window.ShowAndRun()
}