Files
clr-installer/tui/user_manager.go
T
Karthik Prabhu Vinod a5328b689b TUI Net: Add user bundles net-test Note
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>
2020-02-11 11:20:55 -08:00

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{}
}