mirror of
https://github.com/clearlinux/clr-installer.git
synced 2026-09-06 13:41:45 +00:00
Fixes Issue: #635 The user used to mistakenly click on items in the additonal bundles page having to wait long duration unecessarily if the network did not work. This provides the user with a Note saying that selecting an additional bundle requires a working internet connection Signed-off-by: Karthik Prabhu Vinod <karthik.prabhu.vinod@intel.com>
519 lines
14 KiB
Go
519 lines
14 KiB
Go
// Copyright © 2020 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/VladimirMarkelov/clui"
|
|
|
|
"github.com/clearlinux/clr-installer/log"
|
|
"github.com/clearlinux/clr-installer/user"
|
|
)
|
|
|
|
// SelectedUser holds the shared date between the User Manager page
|
|
// and the user configuration page
|
|
type SelectedUser struct {
|
|
user *user.User
|
|
definedUsers []string
|
|
addMode bool
|
|
}
|
|
|
|
const (
|
|
userManagerTitle = "Manage User"
|
|
)
|
|
const (
|
|
userColumnLogin = iota
|
|
userColumnUserName
|
|
userColumnPassword
|
|
userColumnAdmin
|
|
userColumnCount
|
|
)
|
|
|
|
var (
|
|
userColumns []columnInfo
|
|
)
|
|
|
|
func init() {
|
|
userColumns = make([]columnInfo, userColumnCount)
|
|
|
|
userColumns[userColumnLogin].title = "Login"
|
|
userColumns[userColumnLogin].minWidth = 12
|
|
|
|
userColumns[userColumnUserName].title = "User Name"
|
|
userColumns[userColumnUserName].minWidth = -1 // This column get all free space
|
|
|
|
userColumns[userColumnPassword].title = "Password"
|
|
userColumns[userColumnPassword].minWidth = 8
|
|
|
|
userColumns[userColumnAdmin].title = "Admin"
|
|
userColumns[userColumnAdmin].minWidth = 5
|
|
}
|
|
|
|
// UserManagerPage is the Page implementation for the disk partitioning menu page
|
|
type UserManagerPage struct {
|
|
BasePage
|
|
scrollingFrame *clui.Frame // content scrolling frame
|
|
warningLabel *clui.Label // content warning label
|
|
|
|
columnFormat string
|
|
|
|
users []*user.User
|
|
|
|
usersChanged bool
|
|
|
|
rowFrames []*clui.Frame
|
|
activeRow *clui.Frame
|
|
activeUser *SimpleButton
|
|
activeLogin string
|
|
}
|
|
|
|
// GetDone Returns true when there is an admin user
|
|
func (page *UserManagerPage) GetDone() bool {
|
|
for _, user := range page.getModel().Users {
|
|
if user.Admin {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetConfiguredValue Returns the string representation of currently value set
|
|
func (page *UserManagerPage) GetConfiguredValue() string {
|
|
users := page.getModel().Users
|
|
res := []string{}
|
|
|
|
if len(users) == 0 {
|
|
return "No users added"
|
|
}
|
|
|
|
for _, curr := range users {
|
|
tks := []string{curr.Login}
|
|
|
|
if curr.Admin {
|
|
tks = append(tks, "admin")
|
|
}
|
|
|
|
res = append(res, strings.Join(tks, ":"))
|
|
}
|
|
|
|
return strings.Join(res, ", ")
|
|
}
|
|
|
|
// GetConfigDefinition returns if the config was interactively defined by the user,
|
|
// was loaded from a config file or if the config is not set.
|
|
func (page *UserManagerPage) GetConfigDefinition() int {
|
|
users := page.getModel().Users
|
|
if len(users) == 0 {
|
|
return ConfigNotDefined
|
|
}
|
|
|
|
return ConfigDefinedByUser
|
|
}
|
|
|
|
// SetDone put list of users into the model and sets the page as done
|
|
func (page *UserManagerPage) SetDone(done bool) bool {
|
|
page.getModel().RemoveAllUsers()
|
|
for _, user := range page.users {
|
|
page.getModel().AddUser(user)
|
|
}
|
|
|
|
page.usersChanged = false
|
|
page.users = nil
|
|
|
|
// TODO start using new API page.GotoPage() when finished merging
|
|
// disk pages
|
|
page.tui.gotoPage(TuiPageMenu, page)
|
|
|
|
return false
|
|
}
|
|
|
|
// Activate updates the UI elements with the most current list of block devices
|
|
func (page *UserManagerPage) Activate() {
|
|
// We have new or updated user data
|
|
if page.data != nil {
|
|
data := page.data.(*SelectedUser)
|
|
|
|
// Make sure the data is valid before we add
|
|
if data.user.Login != "" {
|
|
page.addUser(data.user)
|
|
page.activeLogin = data.user.Login
|
|
page.data = nil
|
|
} else {
|
|
page.activeLogin = ""
|
|
}
|
|
} else {
|
|
// We are entering from the main menu and need to copy the existing users
|
|
page.removeAllUsers()
|
|
for _, user := range page.getModel().Users {
|
|
page.addUser(user)
|
|
}
|
|
// Since we are restore, there
|
|
page.usersChanged = false
|
|
}
|
|
|
|
page.redrawRows()
|
|
|
|
if page.activeUser != nil {
|
|
page.activated = page.activeUser
|
|
page.scrollToRow(page.activeRow)
|
|
}
|
|
|
|
adminSet := false
|
|
for _, user := range page.users {
|
|
if user.Admin {
|
|
adminSet = true
|
|
}
|
|
}
|
|
|
|
if adminSet {
|
|
page.confirmBtn.SetEnabled(true)
|
|
} else {
|
|
page.confirmBtn.SetEnabled(false)
|
|
}
|
|
|
|
clui.RefreshScreen()
|
|
}
|
|
|
|
func (page *UserManagerPage) scrollToRow(rowFrame *clui.Frame) {
|
|
_, cy, _, ch := page.scrollingFrame.Clipper()
|
|
vx, vy := page.scrollingFrame.Pos()
|
|
|
|
_, ry := rowFrame.Pos()
|
|
_, rh := rowFrame.Size()
|
|
|
|
if ry+rh > cy+ch {
|
|
diff := (cy + ch) - (ry + rh)
|
|
ty := vy + diff
|
|
page.scrollingFrame.ScrollTo(vx, ty)
|
|
} else if ry < cy {
|
|
page.scrollingFrame.ScrollTo(vx, cy)
|
|
}
|
|
}
|
|
|
|
func (page *UserManagerPage) redrawRows() {
|
|
for _, curr := range page.rowFrames {
|
|
curr.Destroy()
|
|
}
|
|
page.rowFrames = []*clui.Frame{}
|
|
adminSet := false
|
|
|
|
if len(page.users) > 0 {
|
|
for _, user := range page.users {
|
|
if user.Admin {
|
|
adminSet = true
|
|
}
|
|
if err := page.addUserRow(user); err != nil {
|
|
page.Panic(err)
|
|
}
|
|
}
|
|
|
|
// Display a warning when no admin user is set
|
|
if !adminSet {
|
|
page.warningLabel.SetTitle("Admin user is required")
|
|
} else {
|
|
page.warningLabel.SetTitle("")
|
|
}
|
|
} else {
|
|
rowFrame := clui.CreateFrame(page.scrollingFrame, 1, AutoSize, clui.BorderNone, clui.Fixed)
|
|
rowFrame.SetPack(clui.Vertical)
|
|
_ = clui.CreateLabel(rowFrame, 2, 1, "*** No Custom Users Defined ***", clui.Fixed)
|
|
page.rowFrames = append(page.rowFrames, rowFrame)
|
|
page.scrollToRow(rowFrame)
|
|
}
|
|
|
|
clui.RefreshScreen()
|
|
}
|
|
|
|
// The user manager page gives the user the option define additional accounts
|
|
// to be created on the target system
|
|
func newUserManagerPage(tui *Tui) (Page, error) {
|
|
page := &UserManagerPage{
|
|
BasePage: BasePage{
|
|
// Tag this Page as required to be complete for the Install to proceed
|
|
required: true,
|
|
},
|
|
}
|
|
page.setupMenu(tui, TuiPageUserManager, userManagerTitle, CancelButton|ConfirmButton, TuiPageMenu)
|
|
|
|
cWidth, cHeight := page.content.Size()
|
|
// Calculate the Scrollable frame area
|
|
sWidth := cWidth - (2 * 2) // Buffer of 2 characters from each side
|
|
sHeight := cHeight + 1 // Add back the blank line from content to menu buttons
|
|
|
|
// Top label for the page
|
|
topFrame := clui.CreateFrame(page.content, sWidth, 2, clui.BorderNone, clui.Fixed)
|
|
topFrame.SetPack(clui.Horizontal)
|
|
|
|
clui.CreateLabel(topFrame, sWidth/2, AutoSize, userManagerTitle, clui.Fixed)
|
|
|
|
page.warningLabel = clui.CreateLabel(topFrame, sWidth/2, AutoSize, "", clui.Fixed)
|
|
page.warningLabel.SetAlign(AlignRight)
|
|
page.warningLabel.SetBackColor(errorLabelBg)
|
|
page.warningLabel.SetTextColor(errorLabelFg)
|
|
|
|
_, lHeight := topFrame.Size()
|
|
sHeight -= lHeight // Remove the label from total height
|
|
|
|
remainingColumns := sWidth - 2
|
|
allFree := -1
|
|
for i, info := range userColumns {
|
|
// Should this column get all extra space?
|
|
if info.minWidth == -1 {
|
|
if allFree == -1 {
|
|
allFree = i
|
|
continue // Do not format this column
|
|
} else {
|
|
log.Warning("More than one user info column set for all free space: %s", info.title)
|
|
info.minWidth = columnWidthDefault
|
|
}
|
|
}
|
|
|
|
l, format := getColumnFormat(info)
|
|
userColumns[i].format = format
|
|
userColumns[i].width = l
|
|
remainingColumns -= l
|
|
}
|
|
|
|
// remove the column spacers
|
|
remainingColumns -= ((len(userColumns) - 1) * len(columnSpacer))
|
|
|
|
// If we had a column which get the remaining space
|
|
if allFree != -1 {
|
|
userColumns[allFree].minWidth = remainingColumns
|
|
userColumns[allFree].width = remainingColumns
|
|
_, userColumns[allFree].format = getColumnFormat(userColumns[allFree])
|
|
}
|
|
|
|
// Build the Header Title and full row format string
|
|
titles := []interface{}{""} // need to use an interface for Sprintf
|
|
formats := []string{}
|
|
dividers := []interface{}{""} // need to use an interface for Sprintf
|
|
for _, info := range userColumns {
|
|
titles = append(titles, info.title)
|
|
formats = append(formats, info.format)
|
|
dividers = append(dividers, strings.Repeat(rowDividor, info.width))
|
|
}
|
|
titles = titles[1:] // pop the first empty string
|
|
page.columnFormat = strings.Join(formats, columnSpacer)
|
|
dividers = dividers[1:] // pop the first empty string
|
|
|
|
// Create the frame for the header label
|
|
headerFrame := clui.CreateFrame(page.content, sWidth, 1, clui.BorderNone, clui.Fixed)
|
|
headerFrame.SetPack(clui.Vertical)
|
|
headerFrame.SetPaddings(1, 0)
|
|
columnsTitle := fmt.Sprintf(page.columnFormat, titles...)
|
|
columnsLabel := clui.CreateLabel(headerFrame, AutoSize, 1, columnsTitle, clui.Fixed)
|
|
columnsLabel.SetPaddings(0, 0)
|
|
_, lHeight = columnsLabel.Size()
|
|
sHeight -= lHeight // Remove the label from total height
|
|
columnsDividors := fmt.Sprintf(page.columnFormat, dividers...)
|
|
columnsDividorLabel := clui.CreateLabel(headerFrame, AutoSize, 1, columnsDividors, clui.Fixed)
|
|
columnsDividorLabel.SetPaddings(0, 0)
|
|
_, lHeight = columnsDividorLabel.Size()
|
|
sHeight -= lHeight // Remove the label from total height
|
|
|
|
page.scrollingFrame = clui.CreateFrame(page.content, sWidth, sHeight, clui.BorderNone, clui.Fixed)
|
|
page.scrollingFrame.SetPack(clui.Vertical)
|
|
page.scrollingFrame.SetScrollable(true)
|
|
page.scrollingFrame.SetGaps(0, 1)
|
|
page.scrollingFrame.SetPaddings(1, 0)
|
|
|
|
// Setup the cancel button with a warning
|
|
page.cancelBtn.OnClick(func(ev clui.Event) {
|
|
if page.usersChanged {
|
|
message := "User data modified and will be lost!\n\nDiscard user changes?"
|
|
title := "Discard Data Changes?"
|
|
if dialog, err := CreateConfirmCancelDialogBox(message, title); err == nil {
|
|
dialog.OnClose(func() {
|
|
if dialog.Confirmed {
|
|
page.data = nil
|
|
page.GotoPage(TuiPageMenu)
|
|
log.Debug("Warning: DATA LOSS: Cancel button " + message)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
page.data = nil
|
|
page.GotoPage(TuiPageMenu)
|
|
}
|
|
})
|
|
|
|
// Add an Add User button
|
|
addButton := CreateSimpleButton(page.cFrame, AutoSize, 1, "Add New User", Fixed)
|
|
addButton.OnClick(func(ev clui.Event) {
|
|
user := &user.User{}
|
|
users := []string{}
|
|
for _, cur := range page.users {
|
|
users = append(users, cur.Login)
|
|
}
|
|
page.data = &SelectedUser{user: user, definedUsers: users, addMode: true}
|
|
page.GotoPage(TuiPageUseradd)
|
|
})
|
|
|
|
// Add a Revert button
|
|
revertBtn := CreateSimpleButton(page.cFrame, AutoSize, AutoSize, "Revert", Fixed)
|
|
revertBtn.OnClick(func(ev clui.Event) {
|
|
if page.usersChanged {
|
|
message := "User data modified and will be lost!\n\nDiscard user changes?"
|
|
title := "Discard Data Changes?"
|
|
if dialog, err := CreateConfirmCancelDialogBox(message, title); err == nil {
|
|
dialog.OnClose(func() {
|
|
if dialog.Confirmed {
|
|
page.removeAllUsers()
|
|
for _, user := range page.getModel().Users {
|
|
page.addUser(user)
|
|
}
|
|
page.usersChanged = false
|
|
page.activeLogin = ""
|
|
|
|
// Clear last selected row as it might be removed
|
|
page.data = nil
|
|
|
|
log.Debug("Warning: DATA LOSS: Revert button " + message)
|
|
page.GotoPage(TuiPageUserManager)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
page.data = nil
|
|
page.activeLogin = ""
|
|
page.GotoPage(TuiPageUserManager)
|
|
}
|
|
})
|
|
|
|
page.activated = addButton
|
|
|
|
return page, nil
|
|
}
|
|
|
|
func (page *UserManagerPage) addUserRow(user *user.User) error {
|
|
rowFrame := clui.CreateFrame(page.scrollingFrame, 1, 1, clui.BorderNone, clui.Fixed)
|
|
rowFrame.SetPack(clui.Vertical)
|
|
|
|
password := ""
|
|
if user.Password != "" {
|
|
password = "********"
|
|
}
|
|
admin := ""
|
|
if user.Admin {
|
|
admin = " X"
|
|
}
|
|
userTitle := fmt.Sprintf(page.columnFormat,
|
|
user.Login, user.UserName, password, admin)
|
|
|
|
userButton := CreateSimpleButton(rowFrame, 1, 1, userTitle, Fixed)
|
|
userButton.SetAlign(AlignLeft)
|
|
|
|
userButton.OnClick(func(ev clui.Event) {
|
|
// Remove the user to be modified as any part of the data could change
|
|
page.removeUser(user)
|
|
|
|
users := []string{}
|
|
for _, cur := range page.users {
|
|
users = append(users, cur.Login)
|
|
}
|
|
|
|
page.data = &SelectedUser{user: user, definedUsers: users, addMode: false}
|
|
|
|
page.GotoPage(TuiPageUseradd)
|
|
})
|
|
|
|
page.rowFrames = append(page.rowFrames, rowFrame)
|
|
|
|
userButton.OnActive(func(active bool) {
|
|
if active {
|
|
page.scrollToRow(rowFrame)
|
|
}
|
|
})
|
|
rowFrame.OnActive(func(active bool) {
|
|
if active {
|
|
page.scrollToRow(rowFrame)
|
|
}
|
|
})
|
|
|
|
// We do not have an active user, so default to the current
|
|
if page.activeLogin == "" {
|
|
page.activeLogin = user.Login
|
|
page.activeUser = userButton
|
|
page.activeRow = rowFrame
|
|
} else if user.Login == page.activeLogin {
|
|
// Set Active User button and Row Frame
|
|
page.activeUser = userButton
|
|
page.activeRow = rowFrame
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sorting Interface
|
|
|
|
// Users is a list of User
|
|
type Users []*user.User
|
|
|
|
// Len is require to find the length of the Users list
|
|
func (users Users) Len() int { return len(users) }
|
|
|
|
// Swap is require for swapping two positions in Users list
|
|
func (users Users) Swap(a, b int) { users[a], users[b] = users[b], users[a] }
|
|
|
|
// ByLogin implements sort.Interface for sorting by the for Login field
|
|
type ByLogin struct{ Users }
|
|
|
|
// Less is require comparison function of the Users list for ByLogin
|
|
func (users ByLogin) Less(a, b int) bool { return users.Users[a].Login < users.Users[b].Login }
|
|
|
|
// ByUserName implements sort.Interface for sorting by the for UserName field
|
|
type ByUserName struct{ Users }
|
|
|
|
// Less is require comparison function of the Users list for ByUserName
|
|
func (users ByUserName) Less(a, b int) bool { return users.Users[a].UserName < users.Users[b].UserName }
|
|
|
|
func (page *UserManagerPage) addUser(addUser *user.User) {
|
|
page.usersChanged = true
|
|
|
|
newUser := &user.User{
|
|
Login: addUser.Login,
|
|
UserName: addUser.UserName,
|
|
Password: addUser.Password,
|
|
Admin: addUser.Admin,
|
|
}
|
|
|
|
page.users = append(page.users, newUser)
|
|
|
|
// Now sort by Login
|
|
sort.Sort(ByLogin{page.users})
|
|
}
|
|
|
|
func (page *UserManagerPage) removeUser(user *user.User) {
|
|
page.usersChanged = true
|
|
|
|
if page.users == nil {
|
|
return
|
|
}
|
|
|
|
deleteIndex := -1
|
|
|
|
for i, cur := range page.users {
|
|
if cur.Equals(user) {
|
|
deleteIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if deleteIndex >= 0 {
|
|
copy(page.users[deleteIndex:], page.users[deleteIndex+1:])
|
|
page.users[len(page.users)-1] = nil
|
|
page.users = page.users[:len(page.users)-1]
|
|
} else {
|
|
log.Warning("Attempting to remove user '%s', but not found in active list.", user.Login)
|
|
}
|
|
}
|
|
func (page *UserManagerPage) removeAllUsers() {
|
|
page.users = []*user.User{}
|
|
}
|