feat(ui): add "no destinations" page
This commit is contained in:
parent
87f6786387
commit
54cfe3a55f
@ -282,6 +282,7 @@ func TestIntegrationDestinationValidations(t *testing.T) {
|
|||||||
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
|
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
|
||||||
|
|
||||||
assert.Contains(t, contents[2], "Status waiting for stream", "expected mediaserver status to be waiting")
|
assert.Contains(t, contents[2], "Status waiting for stream", "expected mediaserver status to be waiting")
|
||||||
|
assert.True(t, contentsIncludes(contents, "No destinations added yet. Press [a] to add a new destination."), "expected to see no destinations message")
|
||||||
},
|
},
|
||||||
2*time.Minute,
|
2*time.Minute,
|
||||||
time.Second,
|
time.Second,
|
||||||
@ -344,6 +345,7 @@ func TestIntegrationDestinationValidations(t *testing.T) {
|
|||||||
|
|
||||||
require.Contains(t, contents[2], "My stream", "expected new destination to be present")
|
require.Contains(t, contents[2], "My stream", "expected new destination to be present")
|
||||||
assert.Contains(t, contents[2], "off-air", "expected new destination to be off-air")
|
assert.Contains(t, contents[2], "off-air", "expected new destination to be off-air")
|
||||||
|
assert.False(t, contentsIncludes(contents, "No destinations added yet. Press [a] to add a new destination."), "expected to not see no destinations message")
|
||||||
},
|
},
|
||||||
10*time.Second,
|
10*time.Second,
|
||||||
time.Second,
|
time.Second,
|
||||||
|
@ -50,15 +50,25 @@ type UI struct {
|
|||||||
screen tcell.Screen
|
screen tcell.Screen
|
||||||
screenCaptureC chan<- ScreenCapture
|
screenCaptureC chan<- ScreenCapture
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
|
container *tview.Flex
|
||||||
sourceViews sourceViews
|
sourceViews sourceViews
|
||||||
destView *tview.Table
|
destView *tview.Table
|
||||||
|
noDestView *tview.TextView
|
||||||
pullProgressModal *tview.Modal
|
pullProgressModal *tview.Modal
|
||||||
|
|
||||||
// other mutable state
|
// other mutable state
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
urlsToStartState map[string]startState
|
urlsToStartState map[string]startState
|
||||||
allowQuit bool
|
|
||||||
|
// allowQuit is true if the user is allowed to quit the app (after the
|
||||||
|
// startup check has completed).
|
||||||
|
allowQuit bool
|
||||||
|
/// addingDestination is true if add destination modal is currently visible.
|
||||||
|
addingDestination bool
|
||||||
|
// hasDestinations is true if the UI thinks there are destinations
|
||||||
|
// configured.
|
||||||
|
hasDestinations bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screen represents a terminal screen. This includes its desired dimensions,
|
// Screen represents a terminal screen. This includes its desired dimensions,
|
||||||
@ -173,7 +183,7 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
sidebar.AddItem(aboutView, 0, 1, false)
|
sidebar.AddItem(aboutView, 0, 1, false)
|
||||||
|
|
||||||
destView := tview.NewTable()
|
destView := tview.NewTable()
|
||||||
destView.SetTitle("Egress streams")
|
destView.SetTitle("Destinations")
|
||||||
destView.SetBorder(true)
|
destView.SetBorder(true)
|
||||||
destView.SetSelectable(true, false)
|
destView.SetSelectable(true, false)
|
||||||
destView.SetWrapSelection(true, false)
|
destView.SetWrapSelection(true, false)
|
||||||
@ -185,13 +195,22 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
flex := tview.NewFlex().
|
container := tview.NewFlex().
|
||||||
SetDirection(tview.FlexColumn).
|
SetDirection(tview.FlexColumn).
|
||||||
AddItem(sidebar, 40, 0, false).
|
AddItem(sidebar, 40, 0, false).
|
||||||
AddItem(destView, 0, 6, false)
|
AddItem(destView, 0, 6, false)
|
||||||
|
|
||||||
|
// noDestView is overlaid on top of the main view when there are no
|
||||||
|
// destinations configured.
|
||||||
|
noDestView := tview.NewTextView().
|
||||||
|
SetText(`No destinations added yet. Press [a] to add a new destination.`).
|
||||||
|
SetTextAlign(tview.AlignCenter).
|
||||||
|
SetTextColor(tcell.ColorGrey)
|
||||||
|
noDestView.SetBorder(false)
|
||||||
|
|
||||||
pages := tview.NewPages()
|
pages := tview.NewPages()
|
||||||
pages.AddPage(pageNameMain, flex, true, true)
|
pages.AddPage(pageNameMain, container, true, true)
|
||||||
|
pages.AddPage(pageNameNoDestinations, noDestView, false, false)
|
||||||
|
|
||||||
app.SetRoot(pages, true)
|
app.SetRoot(pages, true)
|
||||||
app.SetFocus(destView)
|
app.SetFocus(destView)
|
||||||
@ -205,6 +224,7 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
screen: screen,
|
screen: screen,
|
||||||
screenCaptureC: screenCaptureC,
|
screenCaptureC: screenCaptureC,
|
||||||
pages: pages,
|
pages: pages,
|
||||||
|
container: container,
|
||||||
sourceViews: sourceViews{
|
sourceViews: sourceViews{
|
||||||
url: urlTextView,
|
url: urlTextView,
|
||||||
status: statusTextView,
|
status: statusTextView,
|
||||||
@ -215,6 +235,7 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
rx: rxTextView,
|
rx: rxTextView,
|
||||||
},
|
},
|
||||||
destView: destView,
|
destView: destView,
|
||||||
|
noDestView: noDestView,
|
||||||
pullProgressModal: pullProgressModal,
|
pullProgressModal: pullProgressModal,
|
||||||
urlsToStartState: make(map[string]startState),
|
urlsToStartState: make(map[string]startState),
|
||||||
}
|
}
|
||||||
@ -301,7 +322,7 @@ func (ui *UI) ShowSourceNotLiveModal() {
|
|||||||
ui.app.QueueUpdateDraw(func() {
|
ui.app.QueueUpdateDraw(func() {
|
||||||
ui.showModal(
|
ui.showModal(
|
||||||
pageNameModalStartupCheck,
|
pageNameModalStartupCheck,
|
||||||
fmt.Sprintf("Source is not live.\nStart streaming to the source URL then try again:\n\n%s", ui.sourceViews.url.GetText(true)),
|
fmt.Sprintf("Waiting for stream.\nStart streaming to the source URL then try again:\n\n%s", ui.sourceViews.url.GetText(true)),
|
||||||
[]string{"Ok"},
|
[]string{"Ok"},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
@ -324,7 +345,6 @@ func (ui *UI) ShowStartupCheckModal() bool {
|
|||||||
[]string{"Continue", "Exit"},
|
[]string{"Continue", "Exit"},
|
||||||
func(buttonIndex int, _ string) {
|
func(buttonIndex int, _ string) {
|
||||||
if buttonIndex == 0 {
|
if buttonIndex == 0 {
|
||||||
ui.app.SetFocus(ui.destView)
|
|
||||||
done <- true
|
done <- true
|
||||||
} else {
|
} else {
|
||||||
done <- false
|
done <- false
|
||||||
@ -401,6 +421,8 @@ func (ui *UI) SetState(state domain.AppState) {
|
|||||||
for _, dest := range state.Destinations {
|
for _, dest := range state.Destinations {
|
||||||
ui.urlsToStartState[dest.URL] = containerStateToStartState(dest.Container.Status)
|
ui.urlsToStartState[dest.URL] = containerStateToStartState(dest.Container.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.hasDestinations = len(state.Destinations) > 0
|
||||||
ui.mu.Unlock()
|
ui.mu.Unlock()
|
||||||
|
|
||||||
// The state is mutable so can't be passed into QueueUpdateDraw, which
|
// The state is mutable so can't be passed into QueueUpdateDraw, which
|
||||||
@ -474,6 +496,7 @@ func (ui *UI) updateProgressModal(container domain.Container) {
|
|||||||
// on top of other modals.
|
// on top of other modals.
|
||||||
const (
|
const (
|
||||||
pageNameMain = "main"
|
pageNameMain = "main"
|
||||||
|
pageNameNoDestinations = "no-destinations"
|
||||||
pageNameAddDestination = "add-destination"
|
pageNameAddDestination = "add-destination"
|
||||||
pageNameModalAbout = "modal-about"
|
pageNameModalAbout = "modal-about"
|
||||||
pageNameModalQuit = "modal-quit"
|
pageNameModalQuit = "modal-quit"
|
||||||
@ -484,6 +507,21 @@ const (
|
|||||||
pageNameConfigUpdateFailed = "modal-config-update-failed"
|
pageNameConfigUpdateFailed = "modal-config-update-failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (ui *UI) resetFocus() {
|
||||||
|
if name, el := ui.pages.GetFrontPage(); name == pageNameMain || name == pageNameNoDestinations {
|
||||||
|
ui.app.SetFocus(ui.destView)
|
||||||
|
// If we don't explicitly set the focus to some row, then sometimes no row
|
||||||
|
// is selected and the user must press up or down to select a row before
|
||||||
|
// continuing. This isn't completely a blocker but it is sometime a bit
|
||||||
|
// confusing and also makes integration tests less predictable. It would be
|
||||||
|
// nice to improve this behaviour so that e.g. if a new destination is
|
||||||
|
// added then that destination is selected, not the first in the row.
|
||||||
|
ui.destView.Select(1, 0)
|
||||||
|
} else {
|
||||||
|
ui.app.SetFocus(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (ui *UI) showModal(pageName string, text string, buttons []string, doneFunc func(int, string)) {
|
func (ui *UI) showModal(pageName string, text string, buttons []string, doneFunc func(int, string)) {
|
||||||
if ui.pages.HasPage(pageName) {
|
if ui.pages.HasPage(pageName) {
|
||||||
return
|
return
|
||||||
@ -496,10 +534,7 @@ func (ui *UI) showModal(pageName string, text string, buttons []string, doneFunc
|
|||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||||
ui.pages.RemovePage(pageName)
|
ui.pages.RemovePage(pageName)
|
||||||
|
ui.resetFocus()
|
||||||
if name, _ := ui.pages.GetFrontPage(); name == pageNameMain {
|
|
||||||
ui.app.SetFocus(ui.destView)
|
|
||||||
}
|
|
||||||
|
|
||||||
if doneFunc != nil {
|
if doneFunc != nil {
|
||||||
doneFunc(buttonIndex, buttonLabel)
|
doneFunc(buttonIndex, buttonLabel)
|
||||||
@ -558,6 +593,22 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (ui *UI) redrawFromState(state domain.AppState) {
|
func (ui *UI) redrawFromState(state domain.AppState) {
|
||||||
|
var addingDestination bool
|
||||||
|
ui.mu.Lock()
|
||||||
|
addingDestination = ui.addingDestination
|
||||||
|
ui.mu.Unlock()
|
||||||
|
|
||||||
|
var showNoDestinationsPage bool
|
||||||
|
if len(state.Destinations) == 0 && !addingDestination {
|
||||||
|
showNoDestinationsPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if showNoDestinationsPage {
|
||||||
|
x, y, w, _ := ui.destView.GetRect()
|
||||||
|
ui.noDestView.SetRect(x+5, y+4, w-10, 3)
|
||||||
|
ui.pages.ShowPage(pageNameNoDestinations)
|
||||||
|
}
|
||||||
|
|
||||||
headerCell := func(content string, expansion int) *tview.TableCell {
|
headerCell := func(content string, expansion int) *tview.TableCell {
|
||||||
return tview.
|
return tview.
|
||||||
NewTableCell(content).
|
NewTableCell(content).
|
||||||
@ -704,11 +755,7 @@ func (ui *UI) addDestination() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
var currWidth, currHeight int
|
var currWidth, currHeight int
|
||||||
if name, frontPage := ui.pages.GetFrontPage(); name == pageNameMain {
|
_, _, currWidth, currHeight = ui.container.GetRect()
|
||||||
_, _, currWidth, currHeight = frontPage.GetRect()
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form := tview.NewForm()
|
form := tview.NewForm()
|
||||||
form.
|
form.
|
||||||
@ -727,6 +774,11 @@ func (ui *UI) addDestination() {
|
|||||||
SetTitleAlign(tview.AlignLeft).
|
SetTitleAlign(tview.AlignLeft).
|
||||||
SetRect((currWidth-formWidth)/2, (currHeight-formHeight)/2, formWidth, formHeight)
|
SetRect((currWidth-formWidth)/2, (currHeight-formHeight)/2, formWidth, formHeight)
|
||||||
|
|
||||||
|
ui.mu.Lock()
|
||||||
|
ui.addingDestination = true
|
||||||
|
ui.mu.Unlock()
|
||||||
|
|
||||||
|
ui.pages.HidePage(pageNameNoDestinations)
|
||||||
ui.pages.AddPage(pageNameAddDestination, form, false, true)
|
ui.pages.AddPage(pageNameAddDestination, form, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,14 +813,28 @@ func (ui *UI) removeDestination() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UI) DestinationAdded() {
|
func (ui *UI) DestinationAdded() {
|
||||||
|
ui.mu.Lock()
|
||||||
|
ui.hasDestinations = true
|
||||||
|
ui.mu.Unlock()
|
||||||
|
|
||||||
ui.app.QueueUpdateDraw(func() {
|
ui.app.QueueUpdateDraw(func() {
|
||||||
|
ui.pages.HidePage(pageNameNoDestinations)
|
||||||
ui.closeAddDestinationForm()
|
ui.closeAddDestinationForm()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UI) closeAddDestinationForm() {
|
func (ui *UI) closeAddDestinationForm() {
|
||||||
|
var hasDestinations bool
|
||||||
|
ui.mu.Lock()
|
||||||
|
ui.addingDestination = false
|
||||||
|
hasDestinations = ui.hasDestinations
|
||||||
|
ui.mu.Unlock()
|
||||||
|
|
||||||
ui.pages.RemovePage(pageNameAddDestination)
|
ui.pages.RemovePage(pageNameAddDestination)
|
||||||
ui.app.SetFocus(ui.destView)
|
if !hasDestinations {
|
||||||
|
ui.pages.ShowPage(pageNameNoDestinations)
|
||||||
|
}
|
||||||
|
ui.resetFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UI) toggleDestination() {
|
func (ui *UI) toggleDestination() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user