2020-07-29 22:48:27 +00:00
package ui
import (
"context"
"fmt"
2020-09-01 16:06:00 +00:00
"time"
2020-07-29 22:48:27 +00:00
"github.com/cloudflare/cloudflared/logger"
2020-09-01 16:06:00 +00:00
2020-07-29 22:48:27 +00:00
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
type connState struct {
location string
state status
}
type status int
const (
Disconnected status = iota
Connected
2020-08-10 23:09:02 +00:00
Reconnecting
2020-07-24 22:17:17 +00:00
SetUrl
RegisteringTunnel
2020-07-29 22:48:27 +00:00
)
2020-07-24 22:17:17 +00:00
type TunnelEvent struct {
2020-07-29 22:48:27 +00:00
Index uint8
EventType status
Location string
2020-07-24 22:17:17 +00:00
Url string
2020-07-29 22:48:27 +00:00
}
type uiModel struct {
version string
2020-07-24 22:17:17 +00:00
edgeURL string
2020-07-29 22:48:27 +00:00
metricsURL string
proxyURL string
connections [ ] connState
}
type palette struct {
url string
connected string
defaultText string
disconnected string
2020-08-10 23:09:02 +00:00
reconnecting string
2020-07-29 22:48:27 +00:00
}
func NewUIModel ( version , hostname , metricsURL , proxyURL string , haConnections int ) * uiModel {
return & uiModel {
version : version ,
2020-07-24 22:17:17 +00:00
edgeURL : hostname ,
2020-07-29 22:48:27 +00:00
metricsURL : metricsURL ,
proxyURL : proxyURL ,
connections : make ( [ ] connState , haConnections ) ,
}
}
2020-09-01 16:06:00 +00:00
func ( data * uiModel ) LaunchUI (
ctx context . Context ,
log logger . Service ,
logLevels [ ] logger . Level ,
tunnelEventChan <- chan TunnelEvent ,
) {
// Configure the logger to stream logs into the textview
// Add TextView as a group to write output to
logTextView := NewDynamicColorTextView ( )
log . Add ( logTextView , logger . NewUIFormatter ( time . RFC3339 ) , logLevels ... )
// Construct the UI
palette := palette {
url : "lightblue" ,
connected : "lime" ,
defaultText : "white" ,
disconnected : "red" ,
reconnecting : "orange" ,
}
2020-07-29 22:48:27 +00:00
app := tview . NewApplication ( )
grid := tview . NewGrid ( ) . SetGap ( 1 , 0 )
frame := tview . NewFrame ( grid )
header := fmt . Sprintf ( "cloudflared [::b]%s" , data . version )
frame . AddText ( header , true , tview . AlignLeft , tcell . ColorWhite )
// Create table to store connection info and status
connTable := tview . NewTable ( )
// SetColumns takes a value for each column, representing the size of the column
// Numbers <= 0 represent proportional widths and positive numbers represent absolute widths
grid . SetColumns ( 20 , 0 )
// SetRows takes a value for each row, representing the size of the row
2020-08-27 19:04:45 +00:00
grid . SetRows ( 1 , 1 , len ( data . connections ) , 1 , 0 )
2020-07-29 22:48:27 +00:00
// AddItem takes a primitive tview type, row, column, rowSpan, columnSpan, minGridHeight, minGridWidth, and focus
grid . AddItem ( tview . NewTextView ( ) . SetText ( "Tunnel:" ) , 0 , 0 , 1 , 1 , 0 , 0 , false )
grid . AddItem ( tview . NewTextView ( ) . SetText ( "Status:" ) , 1 , 0 , 1 , 1 , 0 , 0 , false )
grid . AddItem ( tview . NewTextView ( ) . SetText ( "Connections:" ) , 2 , 0 , 1 , 1 , 0 , 0 , false )
grid . AddItem ( tview . NewTextView ( ) . SetText ( "Metrics:" ) , 3 , 0 , 1 , 1 , 0 , 0 , false )
2020-07-24 22:17:17 +00:00
tunnelHostText := tview . NewTextView ( ) . SetText ( data . edgeURL )
grid . AddItem ( tunnelHostText , 0 , 1 , 1 , 1 , 0 , 0 , false )
2020-07-29 22:48:27 +00:00
grid . AddItem ( NewDynamicColorTextView ( ) . SetText ( fmt . Sprintf ( "[%s]\u2022[%s] Proxying to [%s::b]%s" , palette . connected , palette . defaultText , palette . url , data . proxyURL ) ) , 1 , 1 , 1 , 1 , 0 , 0 , false )
2020-07-29 22:48:27 +00:00
grid . AddItem ( connTable , 2 , 1 , 1 , 1 , 0 , 0 , false )
2020-09-01 16:06:00 +00:00
grid . AddItem ( NewDynamicColorTextView ( ) . SetText ( fmt . Sprintf ( "Metrics at [%s::b]http://%s/metrics" , palette . url , data . metricsURL ) ) , 3 , 1 , 1 , 1 , 0 , 0 , false )
2020-08-25 23:51:01 +00:00
2020-07-29 22:48:27 +00:00
// Add TextView to stream logs
2020-08-25 23:51:01 +00:00
// Logs are displayed in a new grid so a border can be set around them
logGrid := tview . NewGrid ( ) . SetBorders ( true ) . AddItem ( logTextView . SetChangedFunc ( handleNewText ( app , logTextView ) ) , 0 , 0 , 5 , 2 , 0 , 0 , false )
// LogFrame holds the Logs header as well as the grid with the textView for streamed logs
logFrame := tview . NewFrame ( logGrid ) . AddText ( "[::b]Logs:[::-]" , true , tview . AlignLeft , tcell . ColorWhite ) . SetBorders ( 0 , 0 , 0 , 0 , 0 , 0 )
2020-08-27 17:00:35 +00:00
// Footer for log frame
logFrame . AddText ( "[::d]Use Ctrl+C to exit[::-]" , false , tview . AlignRight , tcell . ColorWhite )
2020-08-25 23:51:01 +00:00
grid . AddItem ( logFrame , 4 , 0 , 5 , 2 , 0 , 0 , false )
2020-07-29 22:48:27 +00:00
go func ( ) {
for {
select {
case <- ctx . Done ( ) :
app . Stop ( )
return
2020-07-24 22:17:17 +00:00
case event := <- tunnelEventChan :
switch event . EventType {
2020-07-29 22:48:27 +00:00
case Connected :
2020-07-24 22:17:17 +00:00
data . setConnTableCell ( event , connTable , palette )
2020-08-10 23:09:02 +00:00
case Disconnected , Reconnecting :
2020-09-01 16:06:00 +00:00
data . changeConnStatus ( event , connTable , log , palette )
2020-07-24 22:17:17 +00:00
case SetUrl :
tunnelHostText . SetText ( event . Url )
data . edgeURL = event . Url
case RegisteringTunnel :
if data . edgeURL == "" {
tunnelHostText . SetText ( "Registering tunnel..." )
}
2020-07-29 22:48:27 +00:00
}
}
app . Draw ( )
}
} ( )
go func ( ) {
if err := app . SetRoot ( frame , true ) . Run ( ) ; err != nil {
2020-09-01 16:06:00 +00:00
log . Errorf ( "Error launching UI: %s" , err )
2020-07-29 22:48:27 +00:00
}
} ( )
}
2020-07-29 22:48:27 +00:00
func NewDynamicColorTextView ( ) * tview . TextView {
2020-07-29 22:48:27 +00:00
return tview . NewTextView ( ) . SetDynamicColors ( true )
}
2020-07-29 22:48:27 +00:00
// Re-draws application when new logs are streamed to UI
func handleNewText ( app * tview . Application , logTextView * tview . TextView ) func ( ) {
return func ( ) {
app . Draw ( )
// SetFocus to enable scrolling in textview
app . SetFocus ( logTextView )
}
}
2020-07-24 22:17:17 +00:00
func ( data * uiModel ) changeConnStatus ( event TunnelEvent , table * tview . Table , logger logger . Service , palette palette ) {
index := int ( event . Index )
2020-08-10 23:09:02 +00:00
// Get connection location and state
2020-07-29 22:48:27 +00:00
connState := data . getConnState ( index )
// Check if connection is already displayed in UI
if connState == nil {
logger . Info ( "Connection is not in the UI table" )
return
}
2020-07-24 22:17:17 +00:00
locationState := event . Location
2020-08-10 23:09:02 +00:00
2020-07-24 22:17:17 +00:00
if event . EventType == Disconnected {
2020-08-10 23:09:02 +00:00
connState . state = Disconnected
2020-07-24 22:17:17 +00:00
} else if event . EventType == Reconnecting {
2020-08-10 23:09:02 +00:00
connState . state = Reconnecting
locationState = "Reconnecting..."
}
2020-07-29 22:48:27 +00:00
connectionNum := index + 1
// Get table cell
cell := table . GetCell ( index , 0 )
2020-08-10 23:09:02 +00:00
// Change dot color in front of text as well as location state
2020-07-24 22:17:17 +00:00
text := newCellText ( palette , connectionNum , locationState , event . EventType )
2020-07-29 22:48:27 +00:00
cell . SetText ( text )
}
// Return connection location and row in UI table
func ( data * uiModel ) getConnState ( connID int ) * connState {
if connID < len ( data . connections ) {
return & data . connections [ connID ]
}
return nil
}
2020-07-24 22:17:17 +00:00
func ( data * uiModel ) setConnTableCell ( event TunnelEvent , table * tview . Table , palette palette ) {
index := int ( event . Index )
2020-07-29 22:48:27 +00:00
connectionNum := index + 1
// Update slice to keep track of connection location and state in UI table
data . connections [ index ] . state = Connected
2020-07-24 22:17:17 +00:00
data . connections [ index ] . location = event . Location
2020-07-29 22:48:27 +00:00
// Update text in table cell to show disconnected state
2020-07-24 22:17:17 +00:00
text := newCellText ( palette , connectionNum , event . Location , event . EventType )
2020-07-29 22:48:27 +00:00
cell := tview . NewTableCell ( text )
table . SetCell ( index , 0 , cell )
}
2020-08-10 23:09:02 +00:00
func newCellText ( palette palette , connectionNum int , location string , connectedStatus status ) string {
2020-09-01 16:06:00 +00:00
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
// where the left middle dot's color depends on the status of the connection
2020-07-29 22:48:27 +00:00
const connFmtString = "[%s]\u2022[%s] #%d: %s"
2020-08-10 23:09:02 +00:00
var dotColor string
switch connectedStatus {
case Connected :
2020-07-29 22:48:27 +00:00
dotColor = palette . connected
2020-08-10 23:09:02 +00:00
case Disconnected :
dotColor = palette . disconnected
case Reconnecting :
dotColor = palette . reconnecting
2020-07-29 22:48:27 +00:00
}
return fmt . Sprintf ( connFmtString , dotColor , palette . defaultText , connectionNum , location )
}