cmd/tailscale: add progress indicator to sign in buttons

For tailscale/tailscale#481

Signed-off-by: Elias Naur <mail@eliasnaur.com>
pull/3/head
Elias Naur 4 years ago
parent e3befb01d8
commit 22851bfa05

@ -44,7 +44,7 @@ type App struct {
// mu protects the following fields. // mu protects the following fields.
mu sync.Mutex mu sync.Mutex
// netState is the most recent network state. // netState is the most recent network state.
netState NetworkState netState BackendState
// browseURL is set whenever the backend wants to // browseURL is set whenever the backend wants to
// browse. // browse.
browseURL *string browseURL *string
@ -54,14 +54,14 @@ type App struct {
type clientState struct { type clientState struct {
browseURL string browseURL string
net NetworkState backend BackendState
// query is the search query, in lowercase. // query is the search query, in lowercase.
query string query string
Peers []UIPeer Peers []UIPeer
} }
type NetworkState struct { type BackendState struct {
State ipn.State State ipn.State
NetworkMap *controlclient.NetworkMap NetworkMap *controlclient.NetworkMap
LostInternet bool LostInternet bool
@ -160,9 +160,12 @@ func (a *App) runBackend() error {
notifications <- n notifications <- n
}) })
}() }()
var cfg *router.Config var (
var state NetworkState cfg *router.Config
var service jni.Object state BackendState
service jni.Object
signingIn bool
)
for { for {
select { select {
case err := <-startErr: case err := <-startErr:
@ -215,6 +218,7 @@ func (a *App) runBackend() error {
a.notify(state) a.notify(state)
} }
if u := n.BrowseToURL; u != nil { if u := n.BrowseToURL; u != nil {
signingIn = false
a.setURL(*u) a.setURL(*u)
} }
if m := n.NetMap; m != nil { if m := n.NetMap; m != nil {
@ -234,7 +238,10 @@ func (a *App) runBackend() error {
case e := <-a.backend: case e := <-a.backend:
switch e := e.(type) { switch e := e.(type) {
case ReauthEvent: case ReauthEvent:
go b.backend.StartLoginInteractive() if !signingIn {
go b.backend.StartLoginInteractive()
signingIn = true
}
case LogoutEvent: case LogoutEvent:
go b.backend.Logout() go b.backend.Logout()
case ConnectEvent: case ConnectEvent:
@ -374,7 +381,7 @@ func (a *App) notifyVPNClosed() {
} }
} }
func (a *App) notify(state NetworkState) { func (a *App) notify(state BackendState) {
a.mu.Lock() a.mu.Lock()
a.netState = state a.netState = state
a.mu.Unlock() a.mu.Unlock()
@ -420,11 +427,12 @@ func (a *App) runUI() error {
a.request(ConnectEvent{Enable: false}) a.request(ConnectEvent{Enable: false})
case <-a.updates: case <-a.updates:
a.mu.Lock() a.mu.Lock()
oldState := state.net.State oldState := state.backend.State
state.net = a.netState state.backend = a.netState
if a.browseURL != nil { if a.browseURL != nil {
state.browseURL = *a.browseURL state.browseURL = *a.browseURL
a.browseURL = nil a.browseURL = nil
ui.signinType = noSignin
} }
if a.prefs != nil { if a.prefs != nil {
ui.enabled.Value = a.prefs.WantRunning ui.enabled.Value = a.prefs.WantRunning
@ -434,7 +442,7 @@ func (a *App) runUI() error {
a.updateState(peer, state) a.updateState(peer, state)
w.Invalidate() w.Invalidate()
if peer != 0 { if peer != 0 {
newState := state.net.State newState := state.backend.State
// Start VPN if we just logged in. // Start VPN if we just logged in.
if oldState <= ipn.Stopped && newState > ipn.Stopped { if oldState <= ipn.Stopped && newState > ipn.Stopped {
if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil { if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil {
@ -444,7 +452,7 @@ func (a *App) runUI() error {
} }
case peer = <-onPeerCreated: case peer = <-onPeerCreated:
w.Invalidate() w.Invalidate()
if state.net.State > ipn.Stopped { if state.backend.State > ipn.Stopped {
if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil { if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil {
return err return err
} }
@ -459,7 +467,7 @@ func (a *App) runUI() error {
return nil return nil
}) })
case <-vpnPrepared: case <-vpnPrepared:
if state.net.State > ipn.Stopped { if state.backend.State > ipn.Stopped {
if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil { if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil {
return err return err
} }
@ -502,7 +510,7 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) {
} }
state.Peers = nil state.Peers = nil
netMap := state.net.NetworkMap netMap := state.backend.NetworkMap
if netMap == nil { if netMap == nil {
return return
} }
@ -534,7 +542,7 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) {
name = strings.ToUpper(name) name = strings.ToUpper(name)
peers = append(peers, UIPeer{Owner: u, Name: name}) peers = append(peers, UIPeer{Owner: u, Name: name})
} }
myID := state.net.NetworkMap.User myID := state.backend.NetworkMap.User
sort.Slice(peers, func(i, j int) bool { sort.Slice(peers, func(i, j int) bool {
lhs, rhs := peers[i], peers[j] lhs, rhs := peers[i], peers[j]
if lu, ru := lhs.Owner, rhs.Owner; ru != lu { if lu, ru := lhs.Owner, rhs.Owner; ru != lu {

@ -51,6 +51,8 @@ type UI struct {
// googleSignin is the button for native Google Sign-in // googleSignin is the button for native Google Sign-in
googleSignin widget.Clickable googleSignin widget.Clickable
signinType signinType
self widget.Clickable self widget.Clickable
peers []widget.Clickable peers []widget.Clickable
@ -86,6 +88,8 @@ type UI struct {
events []UIEvent events []UIEvent
} }
type signinType uint8
// An UIPeer is either a peer or a section header // An UIPeer is either a peer or a section header
// with the user information. // with the user information.
type UIPeer struct { type UIPeer struct {
@ -108,6 +112,12 @@ const (
keyShowIntro = "ui.showintro" keyShowIntro = "ui.showintro"
) )
const (
noSignin signinType = iota
webSignin
googleSignin
)
type ( type (
C = layout.Context C = layout.Context
D = layout.Dimensions D = layout.Dimensions
@ -175,7 +185,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
ui.menu.show = !ui.menu.show ui.menu.show = !ui.menu.show
} }
netmap := state.net.NetworkMap netmap := state.backend.NetworkMap
var localName, localAddr string var localName, localAddr string
var expiry time.Time var expiry time.Time
if netmap != nil { if netmap != nil {
@ -186,12 +196,16 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
} }
} }
if ui.googleSignin.Clicked() { if ui.signinType == noSignin {
ui.events = append(ui.events, GoogleAuthEvent{}) if ui.googleSignin.Clicked() {
} ui.signinType = googleSignin
ui.events = append(ui.events, GoogleAuthEvent{})
}
if ui.webSignin.Clicked() { if ui.webSignin.Clicked() {
ui.events = append(ui.events, ReauthEvent{}) ui.signinType = webSignin
ui.events = append(ui.events, ReauthEvent{})
}
} }
if ui.menuClicked(&ui.menu.copy) && localAddr != "" { if ui.menuClicked(&ui.menu.copy) && localAddr != "" {
@ -215,7 +229,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
const numHeaders = 5 const numHeaders = 5
n := numHeaders + len(state.Peers) n := numHeaders + len(state.Peers)
needsLogin := state.net.State == ipn.NeedsLogin needsLogin := state.backend.State == ipn.NeedsLogin
ui.root.Layout(gtx, n, func(gtx C, idx int) D { ui.root.Layout(gtx, n, func(gtx C, idx int) D {
var in layout.Inset var in layout.Inset
if idx == n-1 { if idx == n-1 {
@ -226,14 +240,14 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
return in.Layout(gtx, func(gtx C) D { return in.Layout(gtx, func(gtx C) D {
switch idx { switch idx {
case 0: case 0:
return ui.layoutTop(gtx, sysIns, &state.net) return ui.layoutTop(gtx, sysIns, &state.backend)
case 1: case 1:
if netmap == nil || state.net.State < ipn.Stopped { if netmap == nil || state.backend.State < ipn.Stopped {
return D{} return D{}
} }
return ui.layoutLocal(gtx, sysIns, localName, localAddr) return ui.layoutLocal(gtx, sysIns, localName, localAddr)
case 2: case 2:
if state.net.State < ipn.Stopped { if state.backend.State < ipn.Stopped {
return D{} return D{}
} }
return ui.layoutSearchbar(gtx, sysIns) return ui.layoutSearchbar(gtx, sysIns)
@ -241,9 +255,9 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat
if !needsLogin { if !needsLogin {
return D{} return D{}
} }
return ui.layoutSignIn(gtx) return ui.layoutSignIn(gtx, &state.backend)
case 4: case 4:
if needsLogin || !state.net.LostInternet { if needsLogin || !state.backend.LostInternet {
return D{} return D{}
} }
return ui.layoutDisconnected(gtx) return ui.layoutDisconnected(gtx)
@ -309,7 +323,7 @@ func (d *Dismiss) Dismissed(gtx layout.Context) bool {
} }
// layoutSignIn lays out the sign in button(s). // layoutSignIn lays out the sign in button(s).
func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions { func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimensions {
return layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D { return layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D {
const ( const (
textColor = 0x555555 textColor = 0x555555
@ -328,24 +342,26 @@ func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions {
signin := material.ButtonLayout(ui.theme, &ui.googleSignin) signin := material.ButtonLayout(ui.theme, &ui.googleSignin)
signin.Background = rgb(white) signin.Background = rgb(white)
return border.Layout(gtx, func(gtx C) D { return ui.withLoader(gtx, ui.signinType == googleSignin, func(gtx C) D {
return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D { return border.Layout(gtx, func(gtx C) D {
return signin.Layout(gtx, func(gtx C) D { return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D {
gtx.Constraints.Max.Y = gtx.Px(unit.Dp(48)) return signin.Layout(gtx, func(gtx C) D {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx, gtx.Constraints.Max.Y = gtx.Px(unit.Dp(48))
layout.Rigid(func(gtx C) D { return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
return layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx C) D { layout.Rigid(func(gtx C) D {
return drawImage(gtx, ui.icons.google, unit.Dp(16)) return layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx C) D {
}) return drawImage(gtx, ui.icons.google, unit.Dp(16))
}), })
layout.Rigid(func(gtx C) D { }),
return layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx C) D { layout.Rigid(func(gtx C) D {
l := material.Body2(ui.theme, "Sign in with Google") return layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx C) D {
l.Color = rgb(textColor) l := material.Body2(ui.theme, "Sign in with Google")
return l.Layout(gtx) l.Color = rgb(textColor)
}) return l.Layout(gtx)
}), })
) }),
)
})
}) })
}) })
}) })
@ -356,13 +372,15 @@ func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions {
if !enableGoogleSignin { if !enableGoogleSignin {
label = "Sign in" label = "Sign in"
} }
return border.Layout(gtx, func(gtx C) D { return ui.withLoader(gtx, ui.signinType == webSignin, func(gtx C) D {
return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D { return border.Layout(gtx, func(gtx C) D {
signin := material.Button(ui.theme, &ui.webSignin, label) return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D {
signin.Background = rgb(signinColor) signin := material.Button(ui.theme, &ui.webSignin, label)
signin.Color = rgb(textColor) signin.Background = rgb(signinColor)
signin.Background = rgb(white) signin.Color = rgb(textColor)
return signin.Layout(gtx) signin.Background = rgb(white)
return signin.Layout(gtx)
})
}) })
}) })
}), }),
@ -370,6 +388,27 @@ func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions {
}) })
} }
func (ui *UI) withLoader(gtx layout.Context, loading bool, w layout.Widget) layout.Dimensions {
cons := gtx.Constraints
return layout.Stack{Alignment: layout.W}.Layout(gtx,
layout.Stacked(func(gtx C) D {
gtx.Constraints = cons
return w(gtx)
}),
layout.Stacked(func(gtx C) D {
if !loading {
return D{}
}
return layout.Inset{Left: unit.Dp(16)}.Layout(gtx, func(gtx C) D {
gtx.Constraints.Min = image.Point{
X: gtx.Px(unit.Dp(16)),
}
return material.Loader(ui.theme).Layout(gtx)
})
}),
)
}
// layoutDisconnected lays out the "please connect to the internet" // layoutDisconnected lays out the "please connect to the internet"
// message. // message.
func (ui *UI) layoutDisconnected(gtx layout.Context) layout.Dimensions { func (ui *UI) layoutDisconnected(gtx layout.Context) layout.Dimensions {
@ -624,7 +663,7 @@ func (ui *UI) layoutSection(gtx layout.Context, sysIns system.Insets, title stri
} }
// layoutTop lays out the top controls: toggle, status and menu dots. // layoutTop lays out the top controls: toggle, status and menu dots.
func (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *NetworkState) layout.Dimensions { func (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *BackendState) layout.Dimensions {
in := layout.Inset{ in := layout.Inset{
Top: unit.Dp(16), Top: unit.Dp(16),
Bottom: unit.Dp(16), Bottom: unit.Dp(16),

@ -4,7 +4,7 @@ go 1.14
require ( require (
eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b
gioui.org v0.0.0-20200630184435-6ef1ff7cfbfb gioui.org v0.0.0-20200709135439-29f820caaac9
gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43 gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43
github.com/go-bindata/go-bindata v3.1.2+incompatible github.com/go-bindata/go-bindata v3.1.2+incompatible
github.com/tailscale/wireguard-go v0.0.0-20200615180905-687c10194779 github.com/tailscale/wireguard-go v0.0.0-20200615180905-687c10194779

@ -3,8 +3,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b h1:J9r7EuPdhvBTafg34EqrObAm/bDEaDh7LvhKJPGficE= eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b h1:J9r7EuPdhvBTafg34EqrObAm/bDEaDh7LvhKJPGficE=
eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b/go.mod h1:CYwJpIhpzVfoHpFXGlXjSx9mXMWtHt4XXmZb6RjumRc= eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b/go.mod h1:CYwJpIhpzVfoHpFXGlXjSx9mXMWtHt4XXmZb6RjumRc=
gioui.org v0.0.0-20200622101735-5368743478e0/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= gioui.org v0.0.0-20200622101735-5368743478e0/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org v0.0.0-20200630184435-6ef1ff7cfbfb h1:+jJBzbEtW03w0+fAfhPCwmN0/8xN1CJ94lbfV2eSKhs= gioui.org v0.0.0-20200709135439-29f820caaac9 h1:dFKWoxIzFlW2e8WTMWYOvGDAosSruMHnS66UBpzdYeU=
gioui.org v0.0.0-20200630184435-6ef1ff7cfbfb/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= gioui.org v0.0.0-20200709135439-29f820caaac9/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43 h1:Wj8OoCIw06dNSSSPAAipnmcG7dbFn+7Et9IY37e1HBU= gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43 h1:Wj8OoCIw06dNSSSPAAipnmcG7dbFn+7Et9IY37e1HBU=
gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43/go.mod h1:KrsGUGWoPetiRyuDmOd/GTNCBFi2u4UbESTFXZ5YqXY= gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43/go.mod h1:KrsGUGWoPetiRyuDmOd/GTNCBFi2u4UbESTFXZ5YqXY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

Loading…
Cancel
Save