Browse Source

Merge pull request #1069 from gravitl/feature_v0.14.0_nc_gui

Feature v0.14.0 nc gui
dcarns 3 years ago
parent
commit
792c04e972

+ 68 - 32
.github/workflows/buildandrelease.yml

@@ -38,7 +38,7 @@ jobs:
           fi
           echo "::set-output name=tag::${{ env.NETMAKER_VERSION }}"
           echo "::set-output name=version::${{ env.PACKAGE_VERSION }}"
-  netmaker:        
+  netmaker:
     runs-on: ubuntu-latest
     needs: version
     steps:
@@ -87,10 +87,12 @@ jobs:
         uses: actions/setup-go@v2
         with:
           go-version: 1.18
-      - name: Build
+
+      - name: Build cli
         run: |
           cd netclient
           env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient main.go
+
       - name: Upload netclient x86 to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
@@ -102,12 +104,29 @@ jobs:
           prerelease: true
           asset_name: netclient
 
+      - name: build gui
+        run: |
+          sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev
+          go build -tags=gui -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-gui .
+
+      - name: Upload netclient x86 gui to Release
+        continue-on-error: true
+        uses: svenstaro/upload-release-action@v2
+        with:
+          repo_token: ${{ secrets.GITHUB_TOKEN }}
+          file: netclient/build/netclient-gui
+          tag: ${{ env.NETMAKER_VERSION }}
+          overwrite: true
+          prerelease: true
+          asset_name: netclient-gui
+
       - name: Package x86 deb
         continue-on-error: true
         uses: gravitl/github-action-fpm@master
         with:
           fpm_args: './netclient/build/netclient=/sbin/netclient ./netclient/build/netclient.service=/lib/systemd/system/netclient.service'
           fpm_opts: '-s dir -t deb --architecture amd64 --version ${{ env.PACKAGE_VERSION }}'
+
       - name: Upload x86 deb to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
@@ -125,12 +144,13 @@ jobs:
         with:
           fpm_args: './netclient/build/netclient=/sbin/netclient ./netclient/build/netclient.service=/lib/systemd/system/netclient.service'
           fpm_opts: '-s dir -t rpm --architecture amd64 --version ${{ env.PACKAGE_VERSION }}'
+
       - name: Upload x86 rpm to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: netclient-${{ env.PACKAGE_VERSION }}-1.x86_64.rpm 
+          file: netclient-${{ env.PACKAGE_VERSION }}-1.x86_64.rpm
           tag: ${{ env.NETMAKER_VERSION }}
           overwrite: true
           prerelease: true
@@ -143,6 +163,7 @@ jobs:
           # arch has particular path requirements --- cannot write to a symbolic link e.g. /sbin and /lib
           fpm_args: './netclient/build/netclient=/usr/bin/netclient ./netclient/build/netclient.service=/usr/lib/systemd/system/netclient.service'
           fpm_opts: '-s dir -t pacman --architecture amd64 --version ${{ env.PACKAGE_VERSION }}'
+
       - name: Upload x86 pacman to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
@@ -177,25 +198,27 @@ jobs:
           env CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-arm6/netclient main.go
           env CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-arm7/netclient main.go
           env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-arm64/netclient main.go
+
       - name: Upload arm5 to Release
         uses: svenstaro/upload-release-action@v2
-        with: 
-          repo_token: ${{ secrets.GITHUB_TOKEN }} 
+        with:
+          repo_token: ${{ secrets.GITHUB_TOKEN }}
           file: netclient/build/netclient-arm5/netclient
           tag: ${{ env.NETMAKER_VERSION }}
-          overwrite: true 
+          overwrite: true
           prerelease: true
           asset_name: netclient-arm5
 
       - name: Upload arm6 to Release
         uses: svenstaro/upload-release-action@v2
-        with: 
+        with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
           file: netclient/build/netclient-arm6/netclient
           tag: ${{ env.NETMAKER_VERSION }}
           overwrite: true
           prerelease: true
           asset_name: netclient-arm6
+
       - name: Upload arm7 to Release
         uses: svenstaro/upload-release-action@v2
         with:
@@ -205,7 +228,7 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-arm7
-     
+
       - name: Upload arm64 to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
@@ -216,6 +239,7 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-arm64
+
       - name: Package arm64 deb
         continue-on-error: true
         uses: gravitl/github-action-fpm@master
@@ -232,12 +256,14 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient_${{ env.PACKAGE_VERSION }}_arm64.deb
+
       - name: Package arm64 rpm
         continue-on-error: true
         uses: gravitl/github-action-fpm@master
         with:
           fpm_args: './netclient/build/netclient-arm64/netclient=/sbin/netclient ./netclient/build/netclient.service=/lib/systemd/netclient.service'
           fpm_opts: '-s dir -t rpm --architecture arm64 --version ${{ env.PACKAGE_VERSION }}'
+
       - name: Upload arm64 rpm to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
@@ -269,6 +295,7 @@ jobs:
         run: |
           cd netclient
           env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle go build -ldflags "-s -w -X 'main.version=$NETMAKER_VERSION'" -o build/netclient-mipsle/netclient main.go && upx build/netclient-mipsle/netclient
+
       - name: Upload mipsle to Release
         uses: svenstaro/upload-release-action@v2
         with:
@@ -278,7 +305,7 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-mipsle
-   
+
   netclient-freebsd:
     runs-on: ubuntu-latest
     needs: version
@@ -312,8 +339,8 @@ jobs:
           tag: ${{ env.NETMAKER_VERSION }}
           overwrite: true
           prerelease: true
-          asset_name: netclient-freebsd      
-          
+          asset_name: netclient-freebsd
+
       - name: Upload freebsd-arm5 to Release
         uses: svenstaro/upload-release-action@v2
         with:
@@ -323,7 +350,7 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-freebsd-arm5
-          
+
       - name: Upload freebsd-arm6 to Release
         uses: svenstaro/upload-release-action@v2
         with:
@@ -333,7 +360,7 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-freebsd-arm6
-          
+
       - name: Upload freebsd-arm7 to Release
         uses: svenstaro/upload-release-action@v2
         with:
@@ -343,7 +370,7 @@ jobs:
           overwrite: true
           prerelease: true
           asset_name: netclient-freebsd-arm7
-          
+
       - name: Upload freebsd-arm64 to Release
         uses: svenstaro/upload-release-action@v2
         with:
@@ -355,7 +382,7 @@ jobs:
           asset_name: netclient-freebsd-arm64
 
   netclient-darwin:
-    runs-on: ubuntu-latest
+    runs-on: macos-latest
     needs: version
     steps:
       - name: Checkout
@@ -366,16 +393,17 @@ jobs:
           VERSION=${{needs.version.outputs.version}}
           echo "NETMAKER_VERSION=${TAG}"  >> $GITHUB_ENV
           echo "PACKAGE_VERSION=${VERSION}" >> $GITHUB_ENV
-      - name: Setup go                                    
-        uses: actions/setup-go@v2                        
-        with:                                           
+      - name: Setup go
+        uses: actions/setup-go@v2
+        with:
           go-version: 1.18
-      - name: Build                                   
-        run: |                                       
-          cd netclient                              
-          env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-darwin/netclient main.go
-          env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-darwin-arm64/netclient main.go
-      - name: Upload darwin-arm64 to Release
+      - name: Build
+        run: |
+          cd netclient
+          env GOOS=darwin GOARCH=amd64 go build -tags=gui -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-darwin/netclient .
+          # arm64 gui version has build constraint  fyne.io issue 2973
+          env GOOS=darwin GOARCH=arm64 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient-darwin-arm64/netclient main.go
+      - name: Upload darwin-amd64 to Release
         uses: svenstaro/upload-release-action@v2
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -396,7 +424,7 @@ jobs:
           asset_name: netclient-darwin-arm64
 
   netclient-windows:
-    runs-on: ubuntu-latest
+    runs-on: windows-latest
     needs: version
     steps:
       - name: Checkout
@@ -407,14 +435,26 @@ jobs:
           VERSION=${{needs.version.outputs.version}}
           echo "NETMAKER_VERSION=${TAG}"  >> $GITHUB_ENV
           echo "PACKAGE_VERSION=${VERSION}" >> $GITHUB_ENV
+        shell: bash
       - name: Setup go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18
+      - name: Mysys2 setup
+        uses: msys2/setup-msys2@v2
+        with:
+          install: >-
+            git
+            mingw-w64-x86_64-toolchain
       - name: Build
         run: |
+          echo $(go env GOPATH)/bin >> $GITHUB_PATH
           cd netclient
-          env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient.exe main.go
+          go get -v github.com/josephspurrier/goversioninfo
+          go install -v github.com/josephspurrier/goversioninfo/cmd/goversioninfo
+          go generate
+          go build -tags=gui -ldflags="-X 'main.version=${NETMAKER_VERSION}'" -o build/netclient.exe .
+
       - name: Upload netclient windows to Release
         continue-on-error: true
         uses: svenstaro/upload-release-action@v2
@@ -428,10 +468,7 @@ jobs:
 
   linux-packages:
     runs-on: ubuntu-latest
-    needs: |
-      version
-      netclient-x86
-      netclien-arm
+    needs: [version, netclient-x86, netclient-arm]
     steps:
       - name: Repository Dispatch
         uses: peter-evans/[email protected]
@@ -440,4 +477,3 @@ jobs:
           repository: gravitl/netmaker-devops
           event-type: build-packages
           client-payload: '{"VERSION": "${{ env.PACKAGE_VERSION }}"}'
-

+ 46 - 6
.github/workflows/test.yml

@@ -12,7 +12,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v2
         with:
-            go-version: 1.18
+          go-version: 1.18
       - name: Build
         run: |
          env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build main.go
@@ -21,13 +21,53 @@ jobs:
          env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build main.go
          env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
          env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
+  linux-gui:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - name: Build
+        run: |
+         sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev
+         env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags=gui main.go
+  mac-gui:
+    runs-on: macos-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - name: Build mac
+        run: |
+          env CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -tags=gui main.go
+  win-gui:
+    runs-on: windows-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.18
+      - name: Mysys2 setup
+        uses: msys2/setup-msys2@v2
+        with:
+          install: >-
+            git
+            mingw-w64-x86_64-toolchain
+      - name: Build win gui
+        run: |
+          env CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags=gui main.go
   tests:
     env:
       DATABASE: sqlite
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: [macos-latest, ubuntu-latest, windows-latest]
+    runs-on: ubuntu-latest
     steps:
       - name: Checkout
         uses: actions/checkout@v2
@@ -37,8 +77,8 @@ jobs:
           go-version: 1.18
       - name: run tests
         run: |
+            sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev
             go test -p 1 ./... -v
         env:
           DATABASE: sqlite
           CLIENT_MODE: "off"
-

+ 11 - 0
go.mod

@@ -31,6 +31,7 @@ require (
 
 require (
 	filippo.io/edwards25519 v1.0.0-rc.1
+	fyne.io/fyne/v2 v2.1.4
 	github.com/guumaster/hostctl v1.1.2
 	github.com/kr/pretty v0.3.0
 	github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0
@@ -47,9 +48,15 @@ require (
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/felixge/httpsnoop v1.0.1 // indirect
+	github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
+	github.com/fsnotify/fsnotify v1.4.9 // indirect
+	github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f // indirect
+	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be // indirect
 	github.com/go-playground/locales v0.14.0 // indirect
 	github.com/go-playground/universal-translator v0.18.0 // indirect
+	github.com/godbus/dbus/v5 v5.0.4 // indirect
 	github.com/gogo/protobuf v1.3.1 // indirect
+	github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
 	github.com/google/go-cmp v0.5.7 // indirect
 	github.com/gorilla/websocket v1.4.2 // indirect
 	github.com/josharian/native v1.0.0 // indirect
@@ -66,7 +73,11 @@ require (
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/seancfoley/bintree v1.0.1 // indirect
 	github.com/spf13/afero v1.3.2 // indirect
+	github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
+	github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
+	github.com/yuin/goldmark v1.3.8 // indirect
+	golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	google.golang.org/appengine v1.4.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect

+ 50 - 0
go.sum

@@ -3,10 +3,15 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
 filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
+fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
+fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
 github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@@ -43,11 +48,20 @@ github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqn
 github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
 github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
+github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
+github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@@ -57,10 +71,14 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
 github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
 github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
+github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
 github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ=
 github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -97,7 +115,9 @@ github.com/guumaster/tablewriter v0.0.9 h1:qyswXhSCI1SWYH78MLApi8AfL8JsWZWAUkZLO
 github.com/guumaster/tablewriter v0.0.9/go.mod h1:9B1xy1BLPtcVAeYjC1EXPxcklqnzk7dU2c3ywGbUnKY=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
 github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
 github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -119,6 +139,7 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
 github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -138,6 +159,8 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCL
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@@ -195,6 +218,10 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM=
+github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
+github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM=
+github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -210,12 +237,16 @@ github.com/txn2/txeh v1.3.0/go.mod h1:O7M6gUTPeMF+vsa4c4Ipx3JDkOYrruB1Wry8QRsMcw
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
 github.com/urfave/cli/v2 v2.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8=
 github.com/urfave/cli/v2 v2.6.0/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
+github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@@ -224,14 +255,18 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -240,9 +275,11 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -255,6 +292,7 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -265,12 +303,17 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -293,6 +336,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -316,6 +363,7 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -326,6 +374,8 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

+ 1 - 1
netclient/command/commands.go

@@ -125,7 +125,7 @@ func Pull(cfg *config.ClientConfig) error {
 
 // List - runs list command from cli
 func List(cfg config.ClientConfig) error {
-	err := functions.List(cfg.Network)
+	_, err := functions.List(cfg.Network)
 	return err
 }
 

+ 1 - 10
netclient/config/config.go

@@ -6,8 +6,6 @@ import (
 	"crypto/ed25519"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/base64"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"log"
@@ -184,14 +182,8 @@ func ReplaceWithBackup(network string) error {
 func GetCLIConfig(c *cli.Context) (ClientConfig, string, error) {
 	var cfg ClientConfig
 	if c.String("token") != "" {
-		tokenbytes, err := base64.StdEncoding.DecodeString(c.String("token"))
+		accesstoken, err := ParseAccessToken(c.String("token"))
 		if err != nil {
-			log.Println("error decoding token")
-			return cfg, "", err
-		}
-		var accesstoken models.AccessToken
-		if err := json.Unmarshal(tokenbytes, &accesstoken); err != nil {
-			log.Println("error converting token json to object", tokenbytes)
 			return cfg, "", err
 		}
 		cfg.Network = accesstoken.ClientConfig.Network
@@ -216,7 +208,6 @@ func GetCLIConfig(c *cli.Context) (ClientConfig, string, error) {
 		if c.String("apiserver") != "" {
 			cfg.Server.API = c.String("apiserver")
 		}
-
 	} else {
 		cfg.Server.AccessKey = c.String("key")
 		cfg.Network = c.String("network")

+ 31 - 0
netclient/config/util.go

@@ -0,0 +1,31 @@
+package config
+
+import (
+	"encoding/base64"
+	"encoding/json"
+
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
+)
+
+var (
+	// GuiActive - indicates if gui is active or not
+	GuiActive = false
+	// GuiRun - holds function for main to call
+	GuiRun interface{}
+)
+
+// ParseAccessToken - used to parse the base64 encoded access token
+func ParseAccessToken(token string) (*models.AccessToken, error) {
+	tokenbytes, err := base64.StdEncoding.DecodeString(token)
+	if err != nil {
+		logger.Log(0, "error decoding token", err.Error())
+		return nil, err
+	}
+	var accesstoken models.AccessToken
+	if err := json.Unmarshal(tokenbytes, &accesstoken); err != nil {
+		logger.Log(0, "error decoding token", err.Error())
+		return nil, err
+	}
+	return &accesstoken, nil
+}

+ 15 - 12
netclient/functions/common.go

@@ -133,6 +133,7 @@ func Uninstall() error {
 			}
 		}
 	}
+	err = nil
 	// clean up OS specific stuff
 	if ncutils.IsWindows() {
 		daemon.CleanupWindows()
@@ -159,19 +160,21 @@ func LeaveNetwork(network string, force bool) error {
 	if node.IsServer != "yes" {
 		token, err := Authenticate(cfg)
 		if err != nil {
-			return fmt.Errorf("unable to authenticate %w", err)
-		}
-		url := "https://" + cfg.Server.API + "/api/nodes/" + cfg.Network + "/" + cfg.Node.ID
-		response, err := API("", http.MethodDelete, url, token)
-		if err != nil {
-			return fmt.Errorf("error deleting node on server %w", err)
-		}
-		if response.StatusCode == http.StatusOK {
-			logger.Log(0, "deleted node", cfg.Node.Name, " on network ", cfg.Network)
+			logger.Log(0, "unable to authenticate: "+err.Error())
 		} else {
-			bodybytes, _ := io.ReadAll(response.Body)
-			defer response.Body.Close()
-			return fmt.Errorf("error deleting node on server %s %s", response.Status, string(bodybytes))
+			url := "https://" + cfg.Server.API + "/api/nodes/" + cfg.Network + "/" + cfg.Node.ID
+			response, err := API("", http.MethodDelete, url, token)
+			if err != nil {
+				logger.Log(0, "error deleting node on server: "+err.Error())
+			} else {
+				if response.StatusCode == http.StatusOK {
+					logger.Log(0, "deleted node", cfg.Node.Name, " on network ", cfg.Network)
+				} else {
+					bodybytes, _ := io.ReadAll(response.Body)
+					defer response.Body.Close()
+					logger.Log(0, fmt.Sprintf("error deleting node on server %s %s", response.Status, string(bodybytes)))
+				}
+			}
 		}
 	}
 	wgClient, wgErr := wgctrl.New()

+ 4 - 4
netclient/functions/list.go

@@ -38,14 +38,14 @@ type address struct {
 }
 
 // List - lists the current peers for the local node with name and node ID
-func List(network string) error {
+func List(network string) ([]Network, error) {
 	nets := []Network{}
 	var err error
 	var networks []string
 	if network == "all" {
 		networks, err = ncutils.GetSystemNetworks()
 		if err != nil {
-			return err
+			return nil, err
 		}
 	} else {
 		networks = append(networks, network)
@@ -55,7 +55,7 @@ func List(network string) error {
 		net, err := getNetwork(network)
 		if err != nil {
 			logger.Log(1, network+": Could not retrieve network configuration.")
-			return err
+			return nil, err
 		}
 		peers, err := getPeers(network)
 		if err == nil && len(peers) > 0 {
@@ -69,7 +69,7 @@ func List(network string) error {
 	}{nets})
 	fmt.Println(string(jsoncfg))
 
-	return nil
+	return nets, nil
 }
 
 func getNetwork(network string) (Network, error) {

+ 33 - 0
netclient/gui/components/buttons.go

@@ -0,0 +1,33 @@
+package components
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/layout"
+	"fyne.io/fyne/v2/widget"
+)
+
+// ColoredButton - renders a colored button with text
+func ColoredButton(text string, tapped func(), color color.Color) *fyne.Container {
+	btn := widget.NewButton(text, tapped)
+	bgColor := canvas.NewRectangle(color)
+	return container.New(
+		layout.NewMaxLayout(),
+		bgColor,
+		btn,
+	)
+}
+
+// ColoredIconButton - renders a colored button with an icon
+func ColoredIconButton(text string, icon fyne.Resource, tapped func(), color color.Color) *fyne.Container {
+	btn := widget.NewButtonWithIcon(text, icon, tapped)
+	bgColor := canvas.NewRectangle(color)
+	return container.New(
+		layout.NewMaxLayout(),
+		btn,
+		bgColor,
+	)
+}

+ 22 - 0
netclient/gui/components/colors.go

@@ -0,0 +1,22 @@
+package components
+
+import "image/color"
+
+var (
+	// Red_color - preferred red color
+	Red_color = color.NRGBA{R: 233, G: 10, B: 17, A: 155}
+	// Gravitl_color - gravitl primary sea green color
+	Gravitl_color = color.NRGBA{R: 14, G: 173, B: 105, A: 155}
+	// Blue_color - preferred blue color
+	Blue_color = color.NRGBA{R: 17, G: 157, B: 164, A: 155}
+	// Danger_color - preferred danger color
+	Danger_color = color.NRGBA{R: 223, G: 71, B: 89, A: 155}
+	// Purple_color - preferred purple color
+	Purple_color = color.NRGBA{R: 115, G: 80, B: 159, A: 155}
+	// Orange_color - preferred orange color
+	Orange_color = color.NRGBA{R: 253, G: 76, B: 65, A: 155}
+	// Grey_color - preferred grey color
+	Grey_color = color.NRGBA{R: 27, G: 27, B: 27, A: 155}
+	// Gold_color - preferred gold color
+	Gold_color = color.NRGBA{R: 218, G: 165, B: 32, A: 155}
+)

+ 23 - 0
netclient/gui/components/text.go

@@ -0,0 +1,23 @@
+package components
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/canvas"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/layout"
+	"fyne.io/fyne/v2/widget"
+)
+
+// ColoredText - renders a colored label
+func ColoredText(text string, color color.Color) *fyne.Container {
+	btn := widget.NewLabel(text)
+	btn.Wrapping = fyne.TextWrapWord
+	bgColor := canvas.NewRectangle(color)
+	return container.New(
+		layout.NewMaxLayout(),
+		bgColor,
+		btn,
+	)
+}

+ 39 - 0
netclient/gui/components/toolbar.go

@@ -0,0 +1,39 @@
+package components
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/widget"
+)
+
+// NewToolbarLabelButton - makes a toolbar button cell with label
+func NewToolbarLabelButton(label string, icon fyne.Resource, onclick func(), colour color.Color) widget.ToolbarItem {
+	l := ColoredIconButton(label, icon, onclick, colour)
+	l.MinSize()
+	return &toolbarLabelButton{l}
+}
+
+// NewToolbarLabel - makes a toolbar text cell
+func NewToolbarLabel(label string) widget.ToolbarItem {
+	l := widget.NewLabel(label)
+	l.MinSize()
+	return &toolbarLabel{l}
+}
+
+type toolbarLabelButton struct {
+	*fyne.Container
+}
+
+type toolbarLabel struct {
+	*widget.Label
+}
+
+func (t *toolbarLabelButton) ToolbarObject() fyne.CanvasObject {
+	return container.NewCenter(t.Container)
+}
+
+func (t *toolbarLabel) ToolbarObject() fyne.CanvasObject {
+	return container.NewCenter(t.Label)
+}

+ 21 - 0
netclient/gui/components/views/confirm.go

@@ -0,0 +1,21 @@
+package views
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+	"github.com/gravitl/netmaker/netclient/gui/components"
+)
+
+// GetConfirmation - displays a confirmation message
+func GetConfirmation(msg string, onCancel, onConfirm func()) fyne.CanvasObject {
+	return container.NewGridWithColumns(1,
+		container.NewCenter(widget.NewLabel(msg)),
+		container.NewCenter(
+			container.NewHBox(
+				components.ColoredIconButton("Confirm", theme.ConfirmIcon(), onConfirm, components.Gravitl_color),
+				components.ColoredIconButton("Cancel", theme.CancelIcon(), onCancel, components.Danger_color),
+			)),
+	)
+}

+ 25 - 0
netclient/gui/components/views/content.go

@@ -0,0 +1,25 @@
+package views
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+// CurrentContent - the content currently being displayed
+var CurrentContent *fyne.Container
+
+// RemoveContent - removes a rendered content
+func RemoveContent(name string) {
+	CurrentContent.Remove(GetView(name))
+}
+
+// AddContent - adds content to be rendered
+func AddContent(name string) {
+	CurrentContent.Add(GetView(name))
+}
+
+// RefreshComponent - refreshes the component to re-render
+func RefreshComponent(name string, c fyne.CanvasObject) {
+	RemoveContent(name)
+	SetView(name, c)
+	AddContent(name)
+}

+ 62 - 0
netclient/gui/components/views/join.go

@@ -0,0 +1,62 @@
+package views
+
+import (
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+	"github.com/gravitl/netmaker/netclient/config"
+	"github.com/gravitl/netmaker/netclient/functions"
+	"github.com/gravitl/netmaker/netclient/gui/components"
+	"github.com/gravitl/netmaker/netclient/ncutils"
+)
+
+// GetJoinView - get's the join screen where a user inputs an access token
+func GetJoinView() fyne.CanvasObject {
+
+	input := widget.NewMultiLineEntry()
+	input.SetPlaceHolder("access token here...")
+
+	submitBtn := components.ColoredIconButton("Submit", theme.UploadIcon(), func() {
+		// ErrorNotify("Could not process token")
+		LoadingNotify()
+		var cfg config.ClientConfig
+		accesstoken, err := config.ParseAccessToken(input.Text)
+		if err != nil {
+			ErrorNotify("Failed to parse access token!")
+			return
+		}
+		cfg.Network = accesstoken.ClientConfig.Network
+		cfg.Node.Network = accesstoken.ClientConfig.Network
+		cfg.Node.Name = ncutils.GetHostname()
+		cfg.Server.AccessKey = accesstoken.ClientConfig.Key
+		cfg.Node.LocalRange = accesstoken.ClientConfig.LocalRange
+		cfg.Server.Server = accesstoken.ServerConfig.Server
+		cfg.Server.API = accesstoken.ServerConfig.APIConnString
+		err = functions.JoinNetwork(&cfg, "")
+		if err != nil {
+			ErrorNotify("Failed to join " + cfg.Network + "!")
+			return
+		}
+		networks, err := ncutils.GetSystemNetworks()
+		if err != nil {
+			ErrorNotify("Failed to read local networks!")
+			return
+		}
+		SuccessNotify("Joined " + cfg.Network + "!")
+		input.Text = ""
+		RefreshComponent(Networks, GetNetworksView(networks))
+		ShowView(Networks)
+		// TODO
+		// - call join
+		// - display loading
+		// - on error display error notification
+		// - on success notify success, refresh networks & networks view, display networks view
+	}, components.Blue_color)
+
+	return container.NewGridWithColumns(1,
+		container.NewCenter(widget.NewLabel("Join new network with Access Token")),
+		input,
+		container.NewCenter(submitBtn),
+	)
+}

+ 167 - 0
netclient/gui/components/views/networks.go

@@ -0,0 +1,167 @@
+package views
+
+import (
+	"fmt"
+	"time"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/layout"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+	"github.com/gravitl/netmaker/netclient/config"
+	"github.com/gravitl/netmaker/netclient/functions"
+	"github.com/gravitl/netmaker/netclient/gui/components"
+	"github.com/gravitl/netmaker/netclient/ncutils"
+)
+
+var currentNetwork *string
+
+// GetNetworksView - displays the view of all networks
+func GetNetworksView(networks []string) fyne.CanvasObject {
+	// renders := []fyne.CanvasObject{}
+	if networks == nil || len(networks) == 0 {
+		return container.NewCenter(widget.NewLabel("No networks present"))
+	}
+	grid := container.New(layout.NewGridLayout(4),
+		container.NewCenter(widget.NewLabel("Network Name")),
+		container.NewCenter(widget.NewLabel("Node Info")),
+		container.NewCenter(widget.NewLabel("Pull Latest")),
+		container.NewCenter(widget.NewLabel("Leave network")),
+	)
+	for i := range networks {
+		network := &networks[i]
+		grid.AddObject(
+			container.NewCenter(widget.NewLabel(*network)),
+		)
+		grid.AddObject(
+			components.ColoredIconButton("info", theme.InfoIcon(), func() {
+				RefreshComponent(NetDetails, GetSingleNetworkView(*network))
+				ShowView(NetDetails)
+			}, components.Gold_color),
+		)
+		grid.AddObject(
+			components.ColoredIconButton("pull", theme.DownloadIcon(), func() {
+				// TODO call pull with network name
+				pull(*network)
+			}, components.Blue_color),
+		)
+		grid.AddObject(
+			components.ColoredIconButton("leave", theme.DeleteIcon(), func() {
+				leave(*network)
+			}, components.Danger_color),
+		)
+		// renders = append(renders, container.NewCenter(netToolbar))
+	}
+
+	return container.NewCenter(grid)
+}
+
+// GetSingleNetworkView - returns details and option to pull a network
+func GetSingleNetworkView(network string) fyne.CanvasObject {
+	if network == "" || len(network) == 0 {
+		return container.NewCenter(widget.NewLabel("No valid network selected"))
+	}
+
+	// == read node values ==
+	LoadingNotify()
+	nets, err := functions.List(network)
+	if err != nil || len(nets) < 1 {
+		return container.NewCenter(widget.NewLabel("No data retrieved."))
+	}
+	var nodecfg config.ClientConfig
+	nodecfg.Network = network
+	nodecfg.ReadConfig()
+	nodeID := nodecfg.Node.ID
+	lastCheckInTime := time.Unix(nodecfg.Node.LastCheckIn, 0)
+	lastCheckIn := lastCheckInTime.Format("2006-01-02 15:04:05")
+	privateAddr := nodecfg.Node.Address
+	privateAddr6 := nodecfg.Node.Address6
+	endpoint := nodecfg.Node.Endpoint
+	health := " (HEALTHY)"
+	if time.Now().After(lastCheckInTime.Add(time.Minute * 5)) {
+		health = " (WARNING)"
+	} else if time.Now().After(lastCheckInTime.Add(time.Minute * 30)) {
+		health = " (ERROR)"
+	}
+	lastCheckIn += health
+	version := nodecfg.Node.Version
+
+	pullBtn := components.ColoredButton("pull "+network, func() { pull(network) }, components.Blue_color)
+	pullBtn.Resize(fyne.NewSize(pullBtn.Size().Width, 50))
+
+	view := container.NewGridWithColumns(1, widget.NewRichTextFromMarkdown(fmt.Sprintf(`### %s
+- ID: %s
+- Last Check In: %s
+- Endpoint: %s
+- Address (IPv4): %s
+- Address6 (IPv6): %s
+- Version: %s
+### Peers
+	`, network, nodeID, lastCheckIn, endpoint, privateAddr, privateAddr6, version)),
+	)
+	netDetailsView := container.NewCenter(
+		view,
+	)
+
+	peerView := container.NewVBox()
+
+	for _, p := range nets[0].Peers {
+		peerString := ""
+		endpointEntry := widget.NewEntry()
+		endpointEntry.Text = fmt.Sprintf("Endpoint: %s", p.PublicEndpoint)
+		endpointEntry.Disable()
+		newEntry := widget.NewEntry()
+		for i, addr := range p.Addresses {
+			if i > 0 && i < len(p.Addresses) {
+				peerString += ", "
+			}
+			peerString += fmt.Sprintf("%s", addr.IP)
+		}
+		newEntry.Text = peerString
+		newEntry.Disable()
+		peerView.AddObject(widget.NewLabel(fmt.Sprintf("Peer: %s", p.PublicKey)))
+		peerView.AddObject(container.NewVBox(container.NewVBox(endpointEntry), container.NewVBox(newEntry)))
+	}
+	peerScroller := container.NewVScroll(peerView)
+	view.AddObject(peerScroller)
+	view.AddObject(container.NewVBox(pullBtn))
+	netDetailsView.Refresh()
+	ClearNotification()
+	return netDetailsView
+}
+
+// == private ==
+func pull(network string) {
+	LoadingNotify()
+	_, err := functions.Pull(network, true)
+	if err != nil {
+		ErrorNotify("Failed to pull " + network + " : " + err.Error())
+	} else {
+		SuccessNotify("Pulled " + network + "!")
+	}
+}
+
+func leave(network string) {
+
+	confirmView := GetConfirmation("Confirm leaving "+network+"?", func() {
+		ShowView(Networks)
+	}, func() {
+		LoadingNotify()
+		err := functions.LeaveNetwork(network, true)
+		if err != nil {
+			ErrorNotify("Failed to leave " + network + " : " + err.Error())
+		} else {
+			SuccessNotify("Left " + network)
+		}
+		networks, err := ncutils.GetSystemNetworks()
+		if err != nil {
+			networks = []string{}
+			ErrorNotify("Failed to read local networks!")
+		}
+		RefreshComponent(Networks, GetNetworksView(networks))
+		ShowView(Networks)
+	})
+	RefreshComponent(Confirm, confirmView)
+	ShowView(Confirm)
+}

+ 38 - 0
netclient/gui/components/views/notification.go

@@ -0,0 +1,38 @@
+package views
+
+import (
+	"image/color"
+
+	"fyne.io/fyne/v2"
+	"github.com/gravitl/netmaker/netclient/gui/components"
+)
+
+// GenerateNotification - generates a notification
+func GenerateNotification(text string, c color.Color) fyne.CanvasObject {
+	return components.ColoredText(text, c)
+}
+
+// ChangeNotification - changes the current notification in the view
+func ChangeNotification(text string, c color.Color) {
+	RefreshComponent(Notify, GenerateNotification(text, c))
+}
+
+// ClearNotification - hides the notifications
+func ClearNotification() {
+	RefreshComponent(Notify, GenerateNotification("", color.Transparent))
+}
+
+// LoadingNotify - changes notification to loading...
+func LoadingNotify() {
+	RefreshComponent(Notify, GenerateNotification("loading...", components.Blue_color))
+}
+
+// ErrorNotify - changes notification to a specified error
+func ErrorNotify(msg string) {
+	RefreshComponent(Notify, GenerateNotification(msg, components.Danger_color))
+}
+
+// SuccessNotify - changes notification to a specified success message
+func SuccessNotify(msg string) {
+	RefreshComponent(Notify, GenerateNotification(msg, components.Gravitl_color))
+}

+ 44 - 0
netclient/gui/components/views/state.go

@@ -0,0 +1,44 @@
+package views
+
+import (
+	"fyne.io/fyne/v2"
+)
+
+var (
+	// Views - the map of all the view components
+	views = make(map[string]fyne.CanvasObject)
+)
+
+const (
+	Networks   = "networks"
+	NetDetails = "netdetails"
+	Notify     = "notification"
+	Join       = "join"
+	Confirm    = "confirm"
+)
+
+// GetView - returns the requested view and sets the CurrentView state
+func GetView(viewName string) fyne.CanvasObject {
+	return views[viewName]
+}
+
+// SetView - sets a view in the views map
+func SetView(viewName string, component fyne.CanvasObject) {
+	views[viewName] = component
+}
+
+// HideView - hides a specific view
+func HideView(viewName string) {
+	views[viewName].Hide()
+}
+
+// ShowView - show's a specific view
+func ShowView(viewName string) {
+	for k := range views {
+		if k == Notify {
+			continue
+		}
+		HideView(k)
+	}
+	views[viewName].Show()
+}

+ 111 - 0
netclient/gui/gui.go

@@ -0,0 +1,111 @@
+package gui
+
+import (
+	"embed"
+	"image/color"
+	"os"
+
+	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/app"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/theme"
+	"fyne.io/fyne/v2/widget"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/netclient/functions"
+	"github.com/gravitl/netmaker/netclient/gui/components"
+	"github.com/gravitl/netmaker/netclient/gui/components/views"
+	"github.com/gravitl/netmaker/netclient/ncutils"
+)
+
+//go:embed nm-logo-sm.png
+var logoContent embed.FS
+
+// Run - run's the netclient GUI
+func Run(networks []string) error {
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Log(0, "No monitor detected, please use CLI commands; use -help for more info.")
+		}
+	}()
+	a := app.New()
+	window := a.NewWindow("Netclient - " + ncutils.Version)
+
+	img, err := logoContent.ReadFile("nm-logo-sm.png")
+	if err != nil {
+		logger.Log(0, "failed to read logo", err.Error())
+		return err
+	}
+
+	window.SetIcon(&fyne.StaticResource{StaticName: "Netmaker logo", StaticContent: img})
+	window.Resize(fyne.NewSize(600, 450))
+
+	networkView := container.NewVScroll(views.GetNetworksView(networks))
+	networkView.SetMinSize(fyne.NewSize(400, 300))
+	views.SetView(views.Networks, networkView)
+
+	netDetailsViews := container.NewVScroll(views.GetSingleNetworkView(""))
+	netDetailsViews.SetMinSize(fyne.NewSize(400, 300))
+	views.SetView(views.NetDetails, netDetailsViews)
+	window.SetFixedSize(false)
+
+	toolbar := container.NewCenter(widget.NewToolbar(
+		components.NewToolbarLabelButton("Networks", theme.HomeIcon(), func() {
+			views.ShowView(views.Networks)
+			views.ClearNotification()
+		}, components.Blue_color),
+		components.NewToolbarLabelButton("Join new", theme.ContentAddIcon(), func() {
+			views.ShowView(views.Join)
+		}, components.Gravitl_color),
+		components.NewToolbarLabelButton("Uninstall", theme.ErrorIcon(), func() {
+			confirmView := views.GetConfirmation("Confirm Netclient uninstall?", func() {
+				views.ShowView(views.Networks)
+			}, func() {
+				views.LoadingNotify()
+				err := functions.Uninstall()
+				if err != nil {
+					views.ErrorNotify("Failed to uninstall: \n" + err.Error())
+				} else {
+					views.SuccessNotify("Uninstalled Netclient!")
+				}
+				networks, err := ncutils.GetSystemNetworks()
+				if err != nil {
+					networks = []string{}
+				}
+				views.RefreshComponent(views.Networks, views.GetNetworksView(networks))
+				views.ShowView(views.Networks)
+			})
+			views.RefreshComponent(views.Confirm, confirmView)
+			views.ShowView(views.Confirm)
+		}, components.Red_color),
+		components.NewToolbarLabelButton("Close", theme.ContentClearIcon(), func() {
+			os.Exit(0)
+		}, components.Purple_color),
+	))
+
+	joinView := views.GetJoinView()
+	views.SetView(views.Join, joinView)
+
+	confirmView := views.GetConfirmation("", func() {}, func() {})
+	views.SetView(views.Confirm, confirmView)
+
+	views.ShowView(views.Networks)
+
+	initialNotification := views.GenerateNotification("", color.Transparent)
+	views.SetView(views.Notify, initialNotification)
+
+	views.CurrentContent = container.NewVBox()
+
+	views.CurrentContent.Add(container.NewGridWithRows(
+		1,
+		toolbar,
+	))
+	views.CurrentContent.Add(views.GetView(views.Networks))
+	views.CurrentContent.Add(views.GetView(views.NetDetails))
+	views.CurrentContent.Add(views.GetView(views.Notify))
+	views.CurrentContent.Add(views.GetView(views.Join))
+
+	window.SetContent(views.CurrentContent)
+	window.ShowAndRun()
+
+	return nil
+}

BIN
netclient/gui/nm-logo-sm.png


+ 10 - 4
netclient/main.go

@@ -1,4 +1,5 @@
-//go:generate goversioninfo -icon=windowsdata/resource/netmaker.ico -manifest=netclient.exe.manifest.xml -64=true -o=netclient.syso
+//go:generate goversioninfo -icon=windowsdata/resource/netclient.ico -manifest=netclient.exe.manifest.xml -64=true -o=netclient.syso
+// -build gui
 
 package main
 
@@ -8,6 +9,7 @@ import (
 	"runtime/debug"
 
 	"github.com/gravitl/netmaker/netclient/cli_options"
+	"github.com/gravitl/netmaker/netclient/config"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/ncwindows"
 	"github.com/urfave/cli/v2"
@@ -35,9 +37,13 @@ func main() {
 		ncutils.CheckWG()
 	}
 
-	err := app.Run(os.Args)
-	if err != nil {
-		log.Fatal(err)
+	if len(os.Args) <= 1 && config.GuiActive {
+		config.GuiRun.(func())()
+	} else {
+		err := app.Run(os.Args)
+		if err != nil {
+			log.Fatal(err)
+		}
 	}
 }
 

+ 22 - 0
netclient/main_gui.go

@@ -0,0 +1,22 @@
+//go:build gui
+// +build gui
+
+package main
+
+import (
+	"github.com/gravitl/netmaker/netclient/config"
+	"github.com/gravitl/netmaker/netclient/gui"
+	"github.com/gravitl/netmaker/netclient/ncutils"
+)
+
+func init() {
+	config.GuiActive = true
+
+	config.GuiRun = func() {
+		networks, err := ncutils.GetSystemNetworks()
+		if err != nil {
+			networks = []string{}
+		}
+		gui.Run(networks)
+	}
+}

+ 4 - 2
netclient/ncutils/netclientutils.go

@@ -24,8 +24,10 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
-// Version - version of the netclient
-var Version = "dev"
+var (
+	// Version - version of the netclient
+	Version = "dev"
+)
 
 // MAX_NAME_LENGTH - maximum node name length
 const MAX_NAME_LENGTH = 62

+ 1 - 1
netclient/netclient.exe.manifest.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
     <assemblyIdentity
-            version="0.13.1.0"
+            version="0.14.0.0"
             processorArchitecture="*"
             name="netclient.exe"
             type="win32"

+ 3 - 3
netclient/versioninfo.json

@@ -2,13 +2,13 @@
     "FixedFileInfo": {
         "FileVersion": {
             "Major": 0,
-            "Minor": 9,
+            "Minor": 14,
             "Patch": 0,
             "Build": 0
         },
         "ProductVersion": {
             "Major": 0,
-            "Minor": 9,
+            "Minor": 14,
             "Patch": 0,
             "Build": 0
         },
@@ -29,7 +29,7 @@
         "OriginalFilename": "",
         "PrivateBuild": "",
         "ProductName": "Netclient",
-        "ProductVersion": "v0.13.1.0",
+        "ProductVersion": "v0.14.0.0",
         "SpecialBuild": ""
     },
     "VarFileInfo": {

BIN
netclient/windowsdata/resource/netclient.ico


BIN
netclient/windowsdata/resource/netmaker.ico