379 lines
13 KiB
Go
379 lines
13 KiB
Go
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()
|
||
}
|