Feat: Add a new CLI command: vela top which can display system information and resource status in UI form (#4556)

* Feat: UI sketch init

Signed-off-by: HanMengnan <1448189829@qq.com>

* Feat: Resource data load finish

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: optimize code struct

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: add test case and fix some bugs
1. add test case to package model
2. fix a bug of ListClusters
3. optimize code structure

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: fix go.mod package version

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: fix some bugs and optimize code structure

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: fix irregular code and add comments

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: supplement test cases and fix bugs

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: move the `vela status --ui` command out and add it as a new command `vela top`

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: fix package import sequence and go.mod

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: add copyright header

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: update description and example of command `vela top`

Signed-off-by: HanMengnan <1448189829@qq.com>

* Fix: convert color variables to const variables

Signed-off-by: HanMengnan <1448189829@qq.com>
This commit is contained in:
Siege Lion
2022-08-09 10:56:22 +08:00
committed by GitHub
parent c3ca308489
commit 4d3ef75601
58 changed files with 3698 additions and 7 deletions
+9 -1
View File
@@ -124,6 +124,11 @@ require (
sigs.k8s.io/gateway-api v0.4.3
)
require (
github.com/gdamore/tcell/v2 v2.5.2
github.com/rivo/tview v0.0.0-20220709181631-73bf2902b59a
)
require github.com/rogpeppe/go-internal v1.8.1
require (
@@ -186,6 +191,7 @@ require (
github.com/fluxcd/pkg/apis/meta v0.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -228,10 +234,11 @@ require (
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -260,6 +267,7 @@ require (
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
+16 -1
View File
@@ -718,6 +718,11 @@ github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui72
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
github.com/gdamore/tcell/v2 v2.5.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY=
github.com/gdamore/tcell/v2 v2.5.2/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
github.com/gertd/go-pluralize v0.1.7 h1:RgvJTJ5W7olOoAks97BOwOlekBFsLEyh00W48Z6ZEZY=
github.com/gertd/go-pluralize v0.1.7/go.mod h1:O4eNeeIf91MHh1GJ2I47DNtaesm66NYvjYgAahcqSDQ=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
@@ -1409,6 +1414,8 @@ github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0U
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lovoo/gcloud-opentracing v0.3.0/go.mod h1:ZFqk2y38kMDDikZPAK7ynTTGuyt17nSPdS3K5e+ZTBY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -1460,8 +1467,9 @@ github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@@ -1844,6 +1852,10 @@ github.com/rancher/wrangler v0.8.3/go.mod h1:dKEaHNB4izxmPUtpq1Hvr3z3Oh+9k5pCZyF
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
github.com/rivo/tview v0.0.0-20220709181631-73bf2902b59a h1:ZjJ1XcvsZkNVO+Rq/vQTOXtN3cmuAgpCp8m4fKG5CkY=
github.com/rivo/tview v0.0.0-20220709181631-73bf2902b59a/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -2566,6 +2578,7 @@ golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -2603,6 +2616,7 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -2615,6 +2629,7 @@ golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJ
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+1 -1
View File
@@ -171,7 +171,7 @@ func (c *AppCollector) ListApplicationResources(app *v1beta1.Application, queryT
klog.Errorf("fetch object for resource apiVersion=%s kind=%s namespace=%s name=%s failure %s, skip this resource", root.APIVersion, root.Kind, root.Namespace, root.Name, err.Error())
continue
}
rootStatus, err := checkResourceStatus(*rootObject)
rootStatus, err := CheckResourceStatus(*rootObject)
if err != nil {
klog.Errorf("check status for resource apiVersion=%s kind=%s namespace=%s name=%s failure %s, skip this resource", root.APIVersion, root.Kind, root.Namespace, root.Name, err.Error())
continue
+3 -2
View File
@@ -525,7 +525,8 @@ var checkServiceStatus = func(obj unstructured.Unstructured) (*types.HealthStatu
return &health, nil
}
func checkResourceStatus(obj unstructured.Unstructured) (*types.HealthStatus, error) {
// CheckResourceStatus return object status data
func CheckResourceStatus(obj unstructured.Unstructured) (*types.HealthStatus, error) {
group := obj.GroupVersionKind().Group
kind := obj.GroupVersionKind().Kind
var checkFunc healthyCheckFunc
@@ -844,7 +845,7 @@ func iterateListSubResources(ctx context.Context, cluster string, k8sClient clie
if !filter(rtn) && len(rtn.LeafNodes) == 0 {
continue
}
healthStatus, err := checkResourceStatus(item)
healthStatus, err := CheckResourceStatus(item)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -383,7 +383,7 @@ func TestHelmResourceStatus(t *testing.T) {
for _, s := range testCases {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(s.hr.DeepCopy())
assert.NoError(t, err)
res, err := checkResourceStatus(unstructured.Unstructured{Object: obj})
res, err := CheckResourceStatus(unstructured.Unstructured{Object: obj})
assert.NoError(t, err)
assert.Equal(t, res, s.res)
}
@@ -430,7 +430,7 @@ func TestHelmRepoResourceStatus(t *testing.T) {
for _, s := range testCases {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(s.hr.DeepCopy())
assert.NoError(t, err)
res, err := checkResourceStatus(unstructured.Unstructured{Object: obj})
res, err := CheckResourceStatus(unstructured.Unstructured{Object: obj})
assert.NoError(t, err)
assert.Equal(t, res, s.res)
}
+1
View File
@@ -86,6 +86,7 @@ func NewCommandWithIOStreams(ioStream util.IOStreams) *cobra.Command {
NewCapabilityShowCommand(commandArgs, ioStream),
// Manage Apps
NewTopCommand(commandArgs, "11", ioStream),
NewListCommand(commandArgs, "10", ioStream),
NewAppStatusCommand(commandArgs, "9", ioStream),
NewDeleteCommand(commandArgs, "7", ioStream),
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/cli/top/view"
)
// NewTopCommand will create command `top` for displaying the platform overview
func NewTopCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "top",
Short: "Launch UI to display the platform overview.",
Long: "Launch UI to display platform overview information and diagnose the status for any specific application.",
Example: ` # Launch UI to display platform overview information and diagnose the status for any specific application
vela top`,
RunE: func(cmd *cobra.Command, args []string) error {
return launchUI(c, cmd)
},
Annotations: map[string]string{
types.TagCommandOrder: order,
types.TagCommandType: types.TypeApp,
},
}
return cmd
}
func launchUI(c common.Args, _ *cobra.Command) error {
k8sClient, err := c.GetClient()
if err != nil {
return fmt.Errorf("cannot get k8s client: %w", err)
}
restConfig, err := c.GetConfig()
if err != nil {
return err
}
app := view.NewApp(k8sClient, restConfig)
app.Init()
return app.Run()
}
+99
View File
@@ -0,0 +1,99 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// App represent the ui of application
type App struct {
*tview.Application
actions model.KeyActions
Components map[string]tview.Primitive
Main *Pages
}
// NewApp return the ui of application
func NewApp() *App {
a := &App{
Application: tview.NewApplication(),
actions: make(model.KeyActions),
Main: NewPages(),
}
a.Components = map[string]tview.Primitive{
"info": NewInfo(),
"menu": NewMenu(),
"logo": NewLogo(),
"crumbs": NewCrumbs(),
}
return a
}
// Init the ui of application init
func (a *App) Init() {
a.SetRoot(a.Main, true)
}
// HasAction judge whether the key has the corresponding action
func (a *App) HasAction(key tcell.Key) (model.KeyAction, bool) {
action, ok := a.actions[key]
return action, ok
}
// AddAction add a new keyAction
func (a *App) AddAction(actions model.KeyActions) {
a.actions.Add(actions)
}
// DelAction delete a keyAction
func (a *App) DelAction(keys []tcell.Key) {
a.actions.Delete(keys)
}
// QueueUpdateDraw queues up a ui action and redraw the ui.
func (a *App) QueueUpdateDraw(f func()) {
if a.Application == nil {
return
}
go func() {
a.Application.QueueUpdateDraw(f)
}()
}
// InfoBoard return system info component
func (a *App) InfoBoard() *InfoBoard {
return a.Components["info"].(*InfoBoard)
}
// Logo return logo component
func (a *App) Logo() *Logo {
return a.Components["logo"].(*Logo)
}
// Menu return key action menu component
func (a *App) Menu() *Menu {
return a.Components["menu"].(*Menu)
}
// Crumbs return the crumbs component
func (a *App) Crumbs() *Crumbs {
return a.Components["crumbs"].(*Crumbs)
}
+63
View File
@@ -0,0 +1,63 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
func TestApp(t *testing.T) {
app := NewApp()
assert.Equal(t, len(app.actions), 0)
assert.Equal(t, len(app.Components), 4)
t.Run("app init", func(t *testing.T) {
app.Init()
app.QueueUpdateDraw(func() {})
})
t.Run("add action", func(t *testing.T) {
app.AddAction(model.KeyActions{
tcell.KeyEnter: model.KeyAction{
Description: "",
Action: nil,
Visible: false,
Shared: false,
},
})
assert.Equal(t, len(app.actions), 1)
})
t.Run("delete action", func(t *testing.T) {
app.DelAction([]tcell.Key{tcell.KeyEnter})
assert.Equal(t, len(app.actions), 0)
})
t.Run("menu", func(t *testing.T) {
assert.NotEmpty(t, app.Menu())
})
t.Run("crumbs", func(t *testing.T) {
assert.NotEmpty(t, app.Crumbs())
})
t.Run("logo", func(t *testing.T) {
assert.NotEmpty(t, app.Logo())
})
t.Run("info board", func(t *testing.T) {
assert.NotEmpty(t, app.InfoBoard())
})
}
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// Crumbs component lay on footer of app and indicate resource level
type Crumbs struct {
*tview.Flex
}
// NewCrumbs return a new crumbs component
func NewCrumbs() *Crumbs {
c := &Crumbs{
Flex: tview.NewFlex(),
}
c.init()
return c
}
func (c *Crumbs) init() {
}
// StackPop change itself when accept "pop" notify from app's main view
func (c *Crumbs) StackPop(_, _ model.Component) {
num := c.GetItemCount()
if num >= 2 {
c.RemoveItem(c.GetItem(num - 1))
c.RemoveItem(c.GetItem(num - 2))
}
}
// StackPush change itself when accept "push" notify from app's main view
func (c *Crumbs) StackPush(component model.Component) {
name := component.Name()
t := tview.NewTextView()
t.SetBackgroundColor(config.CrumbsBackgroundColor)
t.SetTextAlign(tview.AlignCenter)
t.SetText(name)
c.AddItem(t, len(name)+2, 0, false)
c.AddItem(tview.NewTextView(), 1, 0, false)
}
@@ -0,0 +1,37 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCrumbs(t *testing.T) {
crumbs := NewCrumbs()
assert.Equal(t, crumbs.GetItemCount(), 0)
t.Run("stack push", func(t *testing.T) {
p := NewPages()
crumbs.StackPush(p)
assert.Equal(t, crumbs.GetItemCount(), 2)
})
t.Run("stack pop", func(t *testing.T) {
crumbs.StackPop(nil, nil)
assert.Equal(t, crumbs.GetItemCount(), 0)
})
}
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"github.com/rivo/tview"
"k8s.io/client-go/rest"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// InfoBoard a component which display system info
type InfoBoard struct {
*tview.Table
}
// NewInfo return an info component instance
func NewInfo() *InfoBoard {
c := &InfoBoard{
Table: tview.NewTable(),
}
return c
}
// Init info component init
func (board *InfoBoard) Init(restConf *rest.Config) {
var row int
info := model.NewInfo()
board.SetCell(row, 0, board.sectionCell("Context").SetTextColor(config.InfoSectionColor))
board.SetCell(row, 1, infoCell(info.CurrentContext()).SetTextColor(config.InfoTextColor))
row++
board.SetCell(row, 0, board.sectionCell("Cluster Num").SetTextColor(config.InfoSectionColor))
board.SetCell(row, 1, infoCell(info.ClusterNum()).SetTextColor(config.InfoTextColor))
row++
k8s := model.K8SVersion(restConf)
board.SetCell(row, 0, board.sectionCell("K8S Version").SetTextColor(config.InfoSectionColor))
board.SetCell(row, 1, infoCell(k8s).SetTextColor(config.InfoTextColor))
row++
velaCLI := model.VelaCLIVersion()
board.SetCell(row, 0, board.sectionCell("VelaCLI Version").SetTextColor(config.InfoSectionColor))
board.SetCell(row, 1, infoCell(velaCLI).SetTextColor(config.InfoTextColor))
row++
velaCore := model.VelaCoreVersion()
board.SetCell(row, 0, board.sectionCell("VelaCore Version").SetTextColor(config.InfoSectionColor))
board.SetCell(row, 1, infoCell(velaCore).SetTextColor(config.InfoTextColor))
row++
goVersion := model.GOLangVersion()
board.SetCell(row, 0, board.sectionCell("Golang Version").SetTextColor(config.InfoSectionColor))
board.SetCell(row, 1, infoCell(goVersion).SetTextColor(config.InfoTextColor))
}
func (board *InfoBoard) sectionCell(t string) *tview.TableCell {
c := tview.NewTableCell(t + ":")
c.SetAlign(tview.AlignLeft)
return c
}
func infoCell(t string) *tview.TableCell {
c := tview.NewTableCell(t)
return c
}
+41
View File
@@ -0,0 +1,41 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
func TestInfo(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
info := NewInfo()
info.Init(cfg)
assert.Equal(t, info.GetColumnCount(), 2)
assert.Equal(t, info.GetRowCount(), 6)
assert.Equal(t, info.GetCell(0, 0).Text, "Context:")
}
+44
View File
@@ -0,0 +1,44 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import "github.com/gdamore/tcell/v2"
const (
// KeyHelp corresponding value of keyboard key "?"
KeyHelp = 63
// KeySlash corresponding value of keyboard key "/"
KeySlash = 47
// KeyColon corresponding value of keyboard key ":"
KeyColon = 58
// KeySpace corresponding value of keyboard key "SPACE"
KeySpace = 32
)
func init() {
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
tcell.KeyNames[tcell.Key(KeySpace)] = "space"
}
// StandardizeKey standardized combined key event and return corresponding key
func StandardizeKey(event *tcell.EventKey) tcell.Key {
if event.Key() == 256 {
return tcell.Key(event.Rune())
}
return event.Key()
}
+58
View File
@@ -0,0 +1,58 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"fmt"
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/config"
)
var velaLogo = []string{
` _ __ _ __ __ _ `,
`| |/ /_ _ | |__ ___\ \ / /___ | | __ _ `,
`| ' /| | | || '_ \ / _ \\ \ / // _ \| | / _\ |`,
`| . \| |_| || |_) || __/ \ V /| __/| || (_| |`,
`|_|\_\\__,_||_.__/ \___| \_/ \___||_| \__,_|`,
}
// Logo logo component in header
type Logo struct {
*tview.TextView
}
// NewLogo return logo ui component
func NewLogo() *Logo {
l := &Logo{
TextView: tview.NewTextView(),
}
l.init()
return l
}
func (l *Logo) init() {
l.SetWrap(false)
l.SetWordWrap(false)
l.SetTextAlign(tview.AlignCenter)
l.SetTextColor(config.LogoTextColor)
l.SetDynamicColors(true)
for _, line := range velaLogo {
fmt.Fprintf(l, "%s\n", line)
}
}
+35
View File
@@ -0,0 +1,35 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogo(t *testing.T) {
logo := NewLogo()
assert.NotEmpty(t, logo)
assert.Equal(t, logo.GetText(false),
` _ __ _ __ __ _
| |/ /_ _ | |__ ___\ \ / /___ | | __ _
| ' /| | | || '_ \ / _ \\ \ / // _ \| | / _\ |
| . \| |_| || |_) || __/ \ V /| __/| || (_| |
|_|\_\\__,_||_.__/ \___| \_/ \___||_| \__,_|
`)
}
+107
View File
@@ -0,0 +1,107 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"fmt"
"math"
"strconv"
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// Menu is menu component which display key actions of app's main view
type Menu struct {
*tview.Table
}
// NewMenu return a new menu instance
func NewMenu() *Menu {
m := &Menu{
Table: tview.NewTable(),
}
return m
}
// StackPop change itself when accept "pop" notify from app's main view
func (m *Menu) StackPop(old, new model.Component) {
m.UpdateMenu(new.Hint())
}
// StackPush change itself when accept "push" notify from app's main view
func (m *Menu) StackPush(component model.Component) {
m.UpdateMenu(component.Hint())
}
// UpdateMenu update menu component
func (m *Menu) UpdateMenu(hints []model.MenuHint) {
m.Clear()
// convert one-dimensional hints to two-dimensional hints
tableCellStrHint := make([][]model.MenuHint, config.MenuRowNum)
columCount := int(math.Ceil(float64(len(hints)) / config.MenuRowNum))
for row := 0; row < config.MenuRowNum; row++ {
tableCellStrHint[row] = make([]model.MenuHint, columCount)
}
// convert two-dimensional hints to two-dimensional string
tableCellStr := m.buildMenuTable(hints, tableCellStrHint)
for row := 0; row < len(tableCellStr); row++ {
for col := 0; col < len(tableCellStr[row]); col++ {
c := tview.NewTableCell(tableCellStr[row][col])
if len(tableCellStr[row][col]) == 0 {
c = tview.NewTableCell("")
}
m.SetCell(row, col, c)
}
}
}
func (m *Menu) buildMenuTable(hints []model.MenuHint, table [][]model.MenuHint) [][]string {
var row, col int
for _, h := range hints {
table[row][col] = h
row++
if row == config.MenuRowNum {
row, col = 0, col+1
}
}
out := make([][]string, len(table))
for r := range out {
out[r] = make([]string, len(table[r]))
}
for r := range table {
for c := range table[r] {
out[r][c] = formatPlainMenu(table[r][c])
}
}
return out
}
func formatPlainMenu(h model.MenuHint) string {
menuFmt := " [blue:-:b]%-" + strconv.Itoa(10) + "s [:-:b]%s "
return fmt.Sprintf(menuFmt, menuFormat(h), h.Description)
}
func menuFormat(hint model.MenuHint) string {
if len(hint.Key) == 0 && len(hint.Description) == 0 {
return ""
}
return fmt.Sprintf("<%s>", hint.Key)
}
+56
View File
@@ -0,0 +1,56 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
func TestMenu(t *testing.T) {
menu := NewMenu()
table := NewTable()
t.Run("stack push", func(t *testing.T) {
table.actions.Add(model.KeyActions{
tcell.KeyEnter: model.KeyAction{
Description: "",
Action: nil,
Visible: false,
Shared: false,
},
})
menu.StackPush(table)
assert.Equal(t, menu.GetCell(0, 0).Text, " [blue:-:b]<Enter> [:-:b] ")
})
t.Run("stack pop", func(t *testing.T) {
table.actions.Add(model.KeyActions{
tcell.KeyEsc: model.KeyAction{
Description: "",
Action: nil,
Visible: false,
Shared: false,
},
})
menu.StackPop(nil, table)
assert.Equal(t, menu.GetCell(1, 0).Text, " [blue:-:b]<Esc> [:-:b] ")
})
}
+95
View File
@@ -0,0 +1,95 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"fmt"
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// Pages is the app's main content view component
type Pages struct {
*tview.Pages
*model.Stack
}
// NewPages return a page component
func NewPages() *Pages {
p := &Pages{
Pages: tview.NewPages(),
Stack: model.NewStack(),
}
p.Stack.AddListener(p)
return p
}
// Init table component
func (p *Pages) Init() {}
// Name return pages' name
func (p *Pages) Name() string {
return "Pages"
}
// Start table component
func (p *Pages) Start() {}
// Stop table component
func (p *Pages) Stop() {}
// Hint return key action menu hints of the component
func (p *Pages) Hint() []model.MenuHint {
return []model.MenuHint{}
}
// StackPop change itself when accept "pop" notify from app's main view
func (p *Pages) StackPop(old, _ model.Component) {
p.delete(old)
}
// StackPush change itself when accept "push" notify from app's main view
func (p *Pages) StackPush(component model.Component) {
p.addAndShow(component)
}
// AddAndShow adds a new page and bring it to front.
func (p *Pages) addAndShow(c model.Component) {
p.add(c)
p.show(c)
}
func (p *Pages) add(c model.Component) {
p.AddPage(componentID(c), c, true, true)
}
func (p *Pages) delete(c model.Component) {
p.RemovePage(componentID(c))
}
func (p *Pages) show(c model.Component) {
p.SwitchToPage(componentID(c))
}
func componentID(c model.Component) string {
if c.Name() == "" {
panic("Component has no name")
}
return fmt.Sprintf("%s-%p", c.Name(), c)
}
@@ -0,0 +1,53 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPages(t *testing.T) {
pages := NewPages()
table := NewTable()
t.Run("init", func(t *testing.T) {
pages.Init()
pages.Start()
pages.Stop()
assert.Equal(t, pages.Stack.Empty(), true)
})
t.Run("name", func(t *testing.T) {
assert.Equal(t, pages.Name(), "Pages")
})
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(pages.Hint()), 0)
})
t.Run("component id", func(t *testing.T) {
assert.Contains(t, componentID(table), "table")
})
t.Run("stack push", func(t *testing.T) {
pages.StackPush(table)
assert.Equal(t, pages.HasPage(componentID(table)), true)
})
t.Run("stack pop", func(t *testing.T) {
pages.StackPop(table, nil)
assert.Equal(t, pages.HasPage(componentID(table)), false)
})
}
+68
View File
@@ -0,0 +1,68 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// Table is a base table component which can be reused by other component
type Table struct {
*tview.Table
actions model.KeyActions
}
// NewTable return a new table component
func NewTable() *Table {
return &Table{
Table: tview.NewTable(),
actions: make(model.KeyActions),
}
}
// Init table component
func (t *Table) Init() {
t.SetBorder(true)
t.SetBorderAttributes(tcell.AttrItalic)
t.SetBorderPadding(1, 1, 1, 1)
}
// Name return table's name
func (t *Table) Name() string {
return "table"
}
// Start table component
func (t *Table) Start() {
}
// Stop table component
func (t *Table) Stop() {
}
// Hint return key action menu hints of the component
func (t *Table) Hint() []model.MenuHint {
return t.actions.Hint()
}
// Actions return actions
func (t *Table) Actions() model.KeyActions {
return t.actions
}
@@ -0,0 +1,57 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package component
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
func TestTable(t *testing.T) {
table := NewTable()
t.Run("init", func(t *testing.T) {
table.Init()
table.Start()
table.Stop()
})
t.Run("name", func(t *testing.T) {
assert.Equal(t, table.Name(), "table")
})
t.Run("action", func(t *testing.T) {
assert.Equal(t, len(table.Actions()), 0)
})
t.Run("action and hint", func(t *testing.T) {
table.actions.Add(model.KeyActions{
tcell.KeyEnter: model.KeyAction{
Description: "",
Action: nil,
Visible: false,
Shared: false,
},
})
assert.Equal(t, len(table.Actions()), 1)
assert.Equal(t, len(table.Hint()), 1)
assert.Equal(t, table.Hint()[0], model.MenuHint{
Key: "Enter",
Description: "",
})
})
}
+54
View File
@@ -0,0 +1,54 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"github.com/gdamore/tcell/v2"
)
const (
// InfoSectionColor system info component section text color
InfoSectionColor = tcell.ColorRoyalBlue
// InfoTextColor system info component text color
InfoTextColor = tcell.ColorLightGray
// LogoTextColor logo text color
LogoTextColor = tcell.ColorRoyalBlue
// CrumbsBackgroundColor crumbs background color
CrumbsBackgroundColor = tcell.ColorRoyalBlue
// ResourceTableTitleColor resource component title color
ResourceTableTitleColor = tcell.ColorBlue
// ResourceTableHeaderColor resource table header text color
ResourceTableHeaderColor = tcell.ColorLightGray
// ResourceTableBodyColor resource table body text color
ResourceTableBodyColor = tcell.ColorBlue
// ApplicationStartingAndRenderingPhaseColor application Starting and Rendering phase text color
ApplicationStartingAndRenderingPhaseColor = "[blue::]"
// ApplicationWorkflowSuspendingPhaseColor application WorkflowSuspending phase text color
ApplicationWorkflowSuspendingPhaseColor = "[yellow::]"
// ApplicationWorkflowTerminatedPhaseColor application WorkflowTerminated phase text color
ApplicationWorkflowTerminatedPhaseColor = "[red::]"
// ApplicationRunningPhaseColor application Running phase text color
ApplicationRunningPhaseColor = "[green::]"
// ObjectHealthyStatusColor is object Healthy status text color
ObjectHealthyStatusColor = "[green::]"
// ObjectUnhealthyStatusColor is object Unhealthy status text color
ObjectUnhealthyStatusColor = "[red::]"
// ObjectProgressingStatusColor is object Progressing status text color
ObjectProgressingStatusColor = "[blue::]"
// ObjectUnKnownStatusColor is object UnKnown status text color
ObjectUnKnownStatusColor = "[gray::]"
)
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import "k8s.io/client-go/rest"
// Config application configs
type Config struct {
RestConfig *rest.Config
}
+30
View File
@@ -0,0 +1,30 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
const (
// HeaderRowNum ui header max row num
HeaderRowNum = 8
// FooterRowNum ui footer max row num
FooterRowNum = 1
// MenuRowNum menu component max row num
MenuRowNum = 6
// InfoColumnNum info component max column num
InfoColumnNum = 30
// LogoColumnNum logo component max column num
LogoColumnNum = 50
)
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
// Application is the application resource object
type Application struct {
name string
namespace string
phase string
createTime string
}
// ApplicationList is application resource list
type ApplicationList struct {
title []string
data []Application
}
// Header generate header of table in application view
func (l *ApplicationList) Header() []string {
return l.title
}
// Body generate body of table in application view
func (l *ApplicationList) Body() [][]string {
data := make([][]string, 0)
for _, app := range l.data {
data = append(data, []string{app.name, app.namespace, app.phase, app.createTime})
}
return data
}
// ListApplications list all apps in all namespaces
func ListApplications(ctx context.Context, c client.Reader) *ApplicationList {
list := &ApplicationList{title: []string{"Name", "Namespace", "Phase", "CreateTime"}}
apps := v1beta1.ApplicationList{}
namespace := ctx.Value(&CtxKeyNamespace).(string)
if err := c.List(ctx, &apps, client.InNamespace(namespace)); err != nil {
return list
}
for _, app := range apps.Items {
list.data = append(list.data, Application{app.Name, app.Namespace, string(app.Status.Phase), app.CreationTimestamp.String()})
}
return list
}
// LoadApplication load the corresponding application according to name and namespace
func LoadApplication(c client.Client, name, ns string) (*v1beta1.Application, error) {
app := new(v1beta1.Application)
err := c.Get(context.Background(), client.ObjectKey{
Name: name,
Namespace: ns,
}, app)
if err != nil {
return nil, err
}
return app, nil
}
@@ -0,0 +1,64 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
func TestApplicationList_Header(t *testing.T) {
appList := &ApplicationList{title: []string{"Name", "Namespace", "Phase", "CreateTime"}}
assert.Equal(t, appList.Header(), []string{"Name", "Namespace", "Phase", "CreateTime"})
}
func TestApplicationList_Body(t *testing.T) {
appList := &ApplicationList{data: []Application{{"name", "namespace", "phase", "createTime"}}}
assert.Equal(t, len(appList.data), 1)
assert.Equal(t, appList.Body()[0], []string{"name", "namespace", "phase", "createTime"})
}
var _ = Describe("test Application", func() {
var err error
ctx := context.Background()
ctx = context.WithValue(ctx, &CtxKeyNamespace, "")
It("list applications", func() {
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
Expect(err).NotTo(HaveOccurred())
applicationsList := ListApplications(ctx, k8sClient)
Expect(err).NotTo(HaveOccurred())
Expect(applicationsList.Header()).To(Equal([]string{"Name", "Namespace", "Phase", "CreateTime"}))
Expect(len(applicationsList.Body())).To(Equal(1))
})
It("load application info", func() {
application, err := LoadApplication(k8sClient, "first-vela-app", "default")
Expect(err).NotTo(HaveOccurred())
Expect(application.Name).To(Equal("first-vela-app"))
Expect(application.Namespace).To(Equal("default"))
Expect(application.ClusterName).To(Equal(""))
Expect(len(application.Spec.Components)).To(Equal(1))
})
})
+101
View File
@@ -0,0 +1,101 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"strings"
prismclusterv1alpha1 "github.com/kubevela/prism/pkg/apis/cluster/v1alpha1"
"github.com/oam-dev/cluster-gateway/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/multicluster"
)
// Cluster is cluster resource struct
type Cluster struct {
name string
alias string
clusterType string
endpoint string
labels string
}
// ClusterList is cluster resource list
type ClusterList struct {
title []string
data []Cluster
}
// Header generate header of table in application view
func (l *ClusterList) Header() []string {
return l.title
}
// Body generate body of table in application view
func (l *ClusterList) Body() [][]string {
data := make([][]string, 0)
for _, cluster := range l.data {
data = append(data, []string{cluster.name, cluster.alias, cluster.clusterType, cluster.endpoint, cluster.labels})
}
return data
}
// ListClusters list clusters where application deploys resource
func ListClusters(ctx context.Context, c client.Client) *ClusterList {
list := &ClusterList{
title: []string{"Name", "Alias", "Type", "EndPoint", "Labels"},
}
name := ctx.Value(&CtxKeyAppName).(string)
ns := ctx.Value(&CtxKeyNamespace).(string)
app, err := LoadApplication(c, name, ns)
if err != nil {
return list
}
clusterSet := make(map[string]interface{})
for _, svc := range app.Status.AppliedResources {
if svc.Cluster == "" {
clusterSet[multicluster.ClusterLocalName] = struct{}{}
} else {
clusterSet[svc.Cluster] = struct{}{}
}
}
clusters, _ := prismclusterv1alpha1.NewClusterClient(c).List(context.Background())
for _, cluster := range clusters.Items {
if _, ok := clusterSet[cluster.Name]; ok {
clusterInfo := Cluster{
name: cluster.Name,
alias: cluster.Spec.Alias,
clusterType: string(cluster.Spec.CredentialType),
endpoint: cluster.Spec.Endpoint,
}
var labels []string
for k, v := range cluster.Labels {
if !strings.HasPrefix(k, config.MetaApiGroupName) {
labels = append(labels, "[blue::]"+k+"="+"[green::]"+v)
}
}
clusterInfo.labels = strings.Join(labels, ",")
list.data = append(list.data, clusterInfo)
}
}
return list
}
+51
View File
@@ -0,0 +1,51 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"testing"
"github.com/bmizerany/assert"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestClusterList_Header(t *testing.T) {
clusterList := &ClusterList{title: []string{"Name", "Alias", "Type", "EndPoint", "Labels"}}
assert.Equal(t, len(clusterList.Header()), 5)
assert.Equal(t, clusterList.Header(), []string{"Name", "Alias", "Type", "EndPoint", "Labels"})
}
func TestClusterList_Body(t *testing.T) {
clusterList := &ClusterList{data: []Cluster{{"local", "", "", "", ""}}}
assert.Equal(t, len(clusterList.Body()), 1)
assert.Equal(t, clusterList.Body()[0], []string{"local", "", "", "", ""})
}
var _ = Describe("test cluster", func() {
ctx := context.WithValue(context.Background(), &CtxKeyAppName, "first-vela-app")
ctx = context.WithValue(ctx, &CtxKeyNamespace, "default")
It("list clusters", func() {
clusterList := ListClusters(ctx, k8sClient)
Expect(len(clusterList.Header())).To(Equal(5))
Expect(clusterList.Header()).To(Equal([]string{"Name", "Alias", "Type", "EndPoint", "Labels"}))
Expect(len(clusterList.Body())).To(Equal(1))
Expect(clusterList.Body()[0]).To(Equal([]string{"local", "", "Internal", "-", ""}))
})
})
+108
View File
@@ -0,0 +1,108 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"fmt"
"runtime"
"strconv"
"strings"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd/api"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/helm"
"github.com/oam-dev/kubevela/version"
)
// Info is system info struct
type Info struct {
config *api.Config
}
const (
// Unknown info
Unknown = "UNKNOWN"
)
// NewInfo return a new info struct
func NewInfo() *Info {
info := &Info{}
k := genericclioptions.NewConfigFlags(true)
rawConfig, err := k.ToRawKubeConfigLoader().RawConfig()
if err == nil {
info.config = &rawConfig
}
return info
}
// CurrentContext return current context info
func (i *Info) CurrentContext() string {
if i.config != nil {
return i.config.CurrentContext
}
return Unknown
}
// ClusterNum return cluster number
func (i *Info) ClusterNum() string {
if i.config != nil {
return strconv.Itoa(len(i.config.Clusters))
}
return "0"
}
// K8SVersion return k8s version info
func K8SVersion(config *rest.Config) string {
client, err := kubernetes.NewForConfig(config)
if err != nil {
return Unknown
}
serverVersion, err := client.ServerVersion()
if err != nil {
return Unknown
}
vStr := fmt.Sprintf("%s.%s", serverVersion.Major, strings.Replace(serverVersion.Minor, "+", "", 1))
return vStr
}
// VelaCLIVersion return vela cli version info
func VelaCLIVersion() string {
return version.VelaVersion
}
// VelaCoreVersion return vela core version info
func VelaCoreVersion() string {
results, err := helm.GetHelmRelease(types.DefaultKubeVelaNS)
if err != nil {
return Unknown
}
for _, result := range results {
if result.Chart.ChartFullPath() == types.DefaultKubeVelaChartName {
return result.Chart.AppVersion()
}
}
return Unknown
}
// GOLangVersion return golang version info
func GOLangVersion() string {
return runtime.Version()
}
+53
View File
@@ -0,0 +1,53 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
)
func TestInfo_CurrentContext(t *testing.T) {
info := NewInfo()
assert.NotEqual(t, info.CurrentContext(), "UNKNOWN")
}
func TestInfo_ClusterNum(t *testing.T) {
info := NewInfo()
assert.NotEqual(t, info.ClusterNum(), "0")
}
func TestInfo_VelaCLIVersion(t *testing.T) {
assert.NotEmpty(t, VelaCLIVersion())
}
func TestInfo_VelaCoreVersion(t *testing.T) {
assert.NotEmpty(t, VelaCoreVersion())
}
func TestInfo_GOLangVersion(t *testing.T) {
assert.Contains(t, GOLangVersion(), "go")
}
var _ = Describe("test info", func() {
It("test kubernetes version", func() {
Expect(K8SVersion(cfg) != "UNKNOWN").To(BeTrue())
})
})
+102
View File
@@ -0,0 +1,102 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/velaql/providers/query"
)
// K8SObject is k8s resource struct
type K8SObject struct {
name string
namespace string
kind string
apiVersion string
cluster string
status string
}
// K8SObjectList is k8s struct resource list
type K8SObjectList struct {
title []string
data []K8SObject
}
// Header generate header of table in k8s object view
func (l *K8SObjectList) Header() []string {
return l.title
}
// Body generate header of table in k8s object view
func (l *K8SObjectList) Body() [][]string {
data := make([][]string, 0)
for _, app := range l.data {
data = append(data, []string{app.name, app.namespace, app.kind, app.apiVersion, app.cluster, app.status})
}
return data
}
// ListObjects return k8s object resource list
func ListObjects(ctx context.Context, c client.Client) *K8SObjectList {
list := &K8SObjectList{
title: []string{"Name", "Namespace", "Kind", "APIVersion", "Cluster", "Status"},
}
name := ctx.Value(&CtxKeyAppName).(string)
namespace := ctx.Value(&CtxKeyNamespace).(string)
cluster := ctx.Value(&CtxKeyCluster).(string)
opt := query.Option{
Name: name,
Namespace: namespace,
}
if cluster != multicluster.ClusterLocalName {
opt.Filter = query.FilterOption{Cluster: cluster}
}
collector := query.NewAppCollector(c, opt)
appResList, err := collector.CollectResourceFromApp()
if err != nil {
return list
}
for _, resource := range appResList {
list.data = append(list.data, LoadObjectDetail(resource))
}
return list
}
// LoadObjectDetail return the aim k8s object detail info
func LoadObjectDetail(resource query.Resource) K8SObject {
object := K8SObject{
name: resource.Object.GetName(),
namespace: resource.Object.GetNamespace(),
kind: resource.Object.GetKind(),
apiVersion: resource.Object.GetAPIVersion(),
cluster: resource.Cluster,
}
status, err := query.CheckResourceStatus(*resource.Object)
if err == nil {
object.status = string(status.Status)
}
return object
}
@@ -0,0 +1,51 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
)
func TestK8SObjectList_Header(t *testing.T) {
list := K8SObjectList{title: []string{"name", "namespace", "kind", "APIVersion", "cluster", "status"}}
assert.Equal(t, len(list.Header()), 6)
assert.Equal(t, list.Header(), []string{"name", "namespace", "kind", "APIVersion", "cluster", "status"})
}
func TestK8SObjectList_Body(t *testing.T) {
list := K8SObjectList{data: []K8SObject{{"", "", "", "", "", ""}}}
assert.Equal(t, len(list.Body()), 1)
assert.Equal(t, list.Body(), [][]string{{"", "", "", "", "", ""}})
}
var _ = Describe("test k8s object", func() {
ctx := context.Background()
ctx = context.WithValue(ctx, &CtxKeyAppName, "first-vela-app")
ctx = context.WithValue(ctx, &CtxKeyNamespace, "default")
ctx = context.WithValue(ctx, &CtxKeyCluster, "local")
It("list k8s object", func() {
list := ListObjects(ctx, k8sClient)
Expect(len(list.Header())).To(Equal(6))
Expect(len(list.Body())).To(Equal(2))
})
})
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"sort"
"github.com/gdamore/tcell/v2"
)
// KeyAction is key action struct
type KeyAction struct {
Description string
Action func(*tcell.EventKey) *tcell.EventKey
Visible bool
Shared bool
}
// KeyActions is a map from key to action
type KeyActions map[tcell.Key]KeyAction
// Hint convert key action map to menu hints
func (ka KeyActions) Hint() []MenuHint {
tmp := make([]int, 0)
for k := range ka {
tmp = append(tmp, int(k))
}
sort.Ints(tmp)
hints := make([]MenuHint, 0)
for _, key := range tmp {
if name, ok := tcell.KeyNames[tcell.Key(key)]; ok {
hints = append(hints,
MenuHint{
Key: name,
Description: ka[tcell.Key(key)].Description,
},
)
}
}
return hints
}
// Add a key action to key action map
func (ka KeyActions) Add(actions KeyActions) {
for k, v := range actions {
ka[k] = v
}
}
// Set a key action to key action map
func (ka KeyActions) Set(actions KeyActions) {
for k, v := range actions {
ka[k] = v
}
}
// Delete aim key from key action map
func (ka KeyActions) Delete(kk []tcell.Key) {
for _, k := range kk {
delete(ka, k)
}
}
// Clear key action map clear up
func (ka KeyActions) Clear() {
for k := range ka {
delete(ka, k)
}
}
@@ -0,0 +1,63 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
)
func TestKeyActions_Add(t *testing.T) {
actions := KeyActions{tcell.KeyEnter: KeyAction{}}
actions.Add(KeyActions{tcell.KeyTAB: KeyAction{}})
assert.Equal(t, len(actions), 2)
}
func TestKeyActions_Clear(t *testing.T) {
actions := KeyActions{tcell.KeyEnter: KeyAction{}}
actions.Clear()
assert.Equal(t, len(actions), 0)
}
func TestKeyActions_Delete(t *testing.T) {
actions := KeyActions{tcell.KeyEnter: KeyAction{}}
actions.Delete([]tcell.Key{tcell.KeyEnter})
assert.Equal(t, len(actions), 0)
}
func TestKeyActions_Hint(t *testing.T) {
actions := KeyActions{tcell.KeyEnter: KeyAction{}}
hints := actions.Hint()
assert.Equal(t, len(hints), 1)
assert.Equal(t, hints[0], MenuHint{
Key: "Enter",
Description: "",
})
}
func TestKeyActions_Set(t *testing.T) {
actions := KeyActions{tcell.KeyEnter: KeyAction{}}
actions.Set(KeyActions{tcell.KeyHelp: KeyAction{
Description: "",
Action: nil,
Visible: false,
Shared: false,
}})
assert.Equal(t, len(actions), 2)
}
+23
View File
@@ -0,0 +1,23 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
// MenuHint key action hints display by menu component
type MenuHint struct {
Key string
Description string
}
+25
View File
@@ -0,0 +1,25 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
// ResourceList an abstract kinds of resource list which can convert it to data of view in the form of table
type ResourceList interface {
// Header generate header of table in resource view
Header() []string
// Body generate body of table in resource view
Body() [][]string
}
+124
View File
@@ -0,0 +1,124 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import "sync"
const (
// stackPush is the "push" notify type
stackPush = iota
// StackPop is the "pop" notify type
StackPop
)
// ResourceListener listen notify from the main view of app and render itself again
type ResourceListener interface {
// StackPop pop old component and render component
StackPop(Component, Component)
// StackPush push a new component
StackPush(Component)
}
// Stack is a stack to store components and notify listeners of main view of app
type Stack struct {
components []Component
listeners []ResourceListener
mx sync.RWMutex
}
// NewStack return a new stack instance
func NewStack() *Stack {
return &Stack{
components: make([]Component, 0),
listeners: make([]ResourceListener, 0),
}
}
// AddListener add a new resource listener
func (s *Stack) AddListener(listener ResourceListener) {
s.listeners = append(s.listeners, listener)
}
// RemoveListener remove the aim resource listener
func (s *Stack) RemoveListener(listener ResourceListener) {
aimIndex := -1
for index, item := range s.listeners {
if item == listener {
aimIndex = index
}
}
if aimIndex == -1 {
return
}
s.listeners = append(s.listeners[:aimIndex], s.listeners[aimIndex+1:]...)
}
// TopComponent return top component of stack
func (s *Stack) TopComponent() Component {
if s.Empty() {
return nil
}
return s.components[len(s.components)-1]
}
// IsLastComponent check whether stack only have one component now
func (s *Stack) IsLastComponent() bool {
return len(s.components) == 1
}
// PopComponent pop a component from stack
func (s *Stack) PopComponent() {
if s.Empty() {
return
}
s.mx.Lock()
removeComponent := s.components[len(s.components)-1]
s.components = s.components[:len(s.components)-1]
s.mx.Unlock()
s.notifyListener(StackPop, removeComponent)
}
// PushComponent add a new component to stack
func (s *Stack) PushComponent(component Component) {
if top := s.TopComponent(); top != nil {
top.Stop()
}
s.mx.Lock()
s.components = append(s.components, component)
s.mx.Unlock()
s.notifyListener(stackPush, component)
}
// Empty return whether stack is empty
func (s *Stack) Empty() bool {
return len(s.components) == 0
}
func (s *Stack) notifyListener(action int, component Component) {
for _, listener := range s.listeners {
switch action {
case stackPush:
listener.StackPush(component)
case StackPop:
listener.StackPop(component, s.TopComponent())
}
}
}
+58
View File
@@ -0,0 +1,58 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"testing"
"github.com/rivo/tview"
"github.com/stretchr/testify/assert"
)
type mockListener struct{}
func (l *mockListener) StackPop(_ Component, _ Component) {}
func (l *mockListener) StackPush(_ Component) {}
type mockComponent struct {
tview.Primitive
}
func (c *mockComponent) Init() {}
func (c *mockComponent) Name() string {
return ""
}
func (c *mockComponent) Start() {}
func (c *mockComponent) Stop() {}
func (c *mockComponent) Hint() []MenuHint {
return []MenuHint{}
}
func TestStack(t *testing.T) {
stack := NewStack()
assert.Equal(t, stack.Empty(), true)
l := &mockListener{}
stack.AddListener(l)
c := &mockComponent{}
stack.PushComponent(c)
assert.Equal(t, stack.IsLastComponent(), true)
assert.Equal(t, stack.TopComponent(), c)
stack.PopComponent()
assert.Equal(t, stack.Empty(), true)
stack.RemoveListener(l)
assert.Equal(t, len(stack.listeners), 0)
}
+232
View File
@@ -0,0 +1,232 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var createObject = func(name string, ns string, value string, kind string) *unstructured.Unstructured {
o := &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": name,
"namespace": ns,
},
"data": map[string]interface{}{
"key": value,
},
},
}
o.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind(kind))
return o
}
var _ = BeforeSuite(func(done Done) {
By("bootstrapping test environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
CRDDirectoryPaths: []string{
"../../../../charts/vela-core/crds",
},
}
By("start kube test env")
var err error
cfg, err = testEnv.Start()
Expect(err).ShouldNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
By("new kube client")
cfg.Timeout = time.Minute * 2
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
name, namespace := "first-vela-app", "default"
testApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: v1beta1.ApplicationSpec{
Components: []common2.ApplicationComponent{
{
Name: "webservice-test",
Type: "webservice",
},
},
},
}
err = k8sClient.Create(context.TODO(), testApp)
Expect(err).Should(BeNil())
testApp.Status = common2.AppStatus{
AppliedResources: []common2.ClusterObjectReference{
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
Kind: "Ingress",
Namespace: "default",
Name: "ingress-http",
APIVersion: "networking.k8s.io/v1beta1",
},
},
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
Kind: "Ingress",
Namespace: "default",
Name: "ingress-https",
APIVersion: "networking.k8s.io/v1",
},
},
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
Kind: "Ingress",
Namespace: "default",
Name: "ingress-paths",
APIVersion: "networking.k8s.io/v1",
},
},
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
Kind: "Service",
Namespace: "default",
Name: "nodeport",
APIVersion: "v1",
},
},
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
Kind: "Service",
Namespace: "default",
Name: "loadbalancer",
APIVersion: "v1",
},
},
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
Kind: helmapi.HelmReleaseGVK.Kind,
Namespace: "default",
Name: "helmRelease",
},
},
},
}
err = k8sClient.Status().Update(context.TODO(), testApp)
Expect(err).Should(BeNil())
svc := createObject("service1", namespace, "x", "Service")
svcRaw, err := json.Marshal(svc)
Expect(err).Should(Succeed())
dply := createObject("deploy1", namespace, "y", "Deployment")
dplyRaw, err := json.Marshal(dply)
Expect(err).Should(Succeed())
rt := &v1beta1.ResourceTracker{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-v1-%s", name, namespace),
Labels: map[string]string{
oam.LabelAppName: name,
oam.LabelAppNamespace: namespace,
},
Annotations: map[string]string{
oam.AnnotationPublishVersion: "v1",
},
},
Spec: v1beta1.ResourceTrackerSpec{
ManagedResources: []v1beta1.ManagedResource{
{
ClusterObjectReference: common2.ClusterObjectReference{
Cluster: "",
ObjectReference: corev1.ObjectReference{
APIVersion: "v1",
Kind: "Service",
Namespace: namespace,
Name: "web",
},
},
OAMObjectReference: common2.OAMObjectReference{
Component: "web",
},
Data: &runtime.RawExtension{Raw: svcRaw},
},
{
ClusterObjectReference: common2.ClusterObjectReference{
Cluster: "",
ObjectReference: corev1.ObjectReference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: namespace,
Name: "web",
},
},
OAMObjectReference: common2.OAMObjectReference{
Component: "web",
},
Data: &runtime.RawExtension{Raw: dplyRaw},
},
},
Type: v1beta1.ResourceTrackerTypeVersioned,
},
}
Expect(k8sClient.Create(context.TODO(), rt)).Should(BeNil())
close(done)
}, 240)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
func TestModel(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Model Suite")
}
+59
View File
@@ -0,0 +1,59 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import "github.com/rivo/tview"
type (
// Component is an abstract of component of app
Component interface {
Primitive
Initer
Hinter
}
// Primitive is an abstract of tview ui component
Primitive interface {
tview.Primitive
// Name return name of the component
Name() string
}
// Initer is an abstract of components whose need to init
Initer interface {
// Start the component
Start()
// Stop the component
Stop()
// Init the component
Init()
}
// Hinter is an abstract of components which can provide menu hints to menu component
Hinter interface {
// Hint return key action menu hints of the component
Hint() []MenuHint
}
)
var (
// CtxKeyAppName request context key of application name
CtxKeyAppName = "appName"
// CtxKeyCluster request context key of cluster name
CtxKeyCluster = "cluster"
// CtxKeyNamespace request context key of namespace name
CtxKeyNamespace = "appNs"
)
+152
View File
@@ -0,0 +1,152 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/references/cli/top/component"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// App application object
type App struct {
*component.App
client client.Client
config config.Config
command *Command
content *PageStack
}
// NewApp return a new app object
func NewApp(c client.Client, restConfig *rest.Config) *App {
a := &App{
App: component.NewApp(),
client: c,
config: config.Config{
RestConfig: restConfig,
},
}
a.command = NewCommand(a)
a.content = NewPageStack(a)
return a
}
// Init the app
func (a *App) Init() {
a.command.Init()
a.content.Init()
a.content.AddListener(a.Menu())
a.content.AddListener(a.Crumbs())
a.App.Init()
a.layout()
a.bindKeys()
a.SetInputCapture(a.keyboard)
a.defaultView(nil)
}
func (a *App) layout() {
main := tview.NewFlex().SetDirection(tview.FlexRow)
main.SetBorder(true)
main.SetBorderPadding(0, 0, 1, 1)
main.AddItem(a.buildHeader(), config.HeaderRowNum, 1, false)
main.AddItem(a.content, 0, 3, true)
main.AddItem(a.Crumbs(), config.FooterRowNum, 1, false)
a.Main.AddPage("main", main, true, false)
}
func (a *App) buildHeader() tview.Primitive {
header := tview.NewFlex()
header.SetDirection(tview.FlexColumn)
info := a.InfoBoard()
info.Init(a.config.RestConfig)
header.AddItem(info, config.InfoColumnNum, 1, false)
header.AddItem(a.Menu(), 0, 2, false)
header.AddItem(a.Logo(), config.LogoColumnNum, 1, false)
return header
}
// Run is the application running entrance
func (a *App) Run() error {
go func() {
a.QueueUpdateDraw(func() {
a.Main.SwitchToPage("main")
})
}()
err := a.Application.Run()
if err != nil {
return err
}
return nil
}
func (a *App) bindKeys() {
a.AddAction(model.KeyActions{
tcell.KeyESC: model.KeyAction{Description: "Back", Action: a.Back, Visible: true, Shared: true},
component.KeyHelp: model.KeyAction{Description: "Help", Action: a.helpView, Visible: true, Shared: true},
})
}
func (a *App) keyboard(event *tcell.EventKey) *tcell.EventKey {
if action, ok := a.HasAction(component.StandardizeKey(event)); ok {
return action.Action(event)
}
return event
}
// inject add a new component to the app's main view to refresh the content of the main view
func (a *App) inject(c model.Component) {
c.Init()
a.content.PushComponent(c)
}
// defaultView is the first view of running application
func (a *App) defaultView(event *tcell.EventKey) *tcell.EventKey {
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyNamespace, "")
a.command.run(ctx, "app")
return event
}
// helpView to display the view after pressing the Help key(?)
func (a *App) helpView(_ *tcell.EventKey) *tcell.EventKey {
top := a.content.TopComponent()
if top != nil && top.Name() == "Help" {
a.content.PopComponent()
return nil
}
helpView := NewHelpView(a)
a.inject(helpView)
return nil
}
// Back to return before view corresponding to the ESC key
func (a *App) Back(_ *tcell.EventKey) *tcell.EventKey {
if !a.content.IsLastComponent() {
a.content.PopComponent()
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"testing"
"time"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
func TestApp(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
assert.Equal(t, len(app.Components), 4)
t.Run("init", func(t *testing.T) {
app.Init()
assert.Equal(t, app.Main.HasPage("main"), true)
_, ok := app.HasAction(tcell.KeyESC)
assert.Equal(t, ok, true)
app.content.Stack.RemoveListener(app.Crumbs())
assert.NotEmpty(t, app.content.Stack.TopComponent())
assert.Equal(t, app.content.Stack.Empty(), false)
assert.Equal(t, app.content.Stack.IsLastComponent(), true)
})
t.Run("keyboard", func(t *testing.T) {
evt1 := tcell.NewEventKey(tcell.KeyEsc, '/', 0)
assert.Empty(t, app.keyboard(evt1))
evt2 := tcell.NewEventKey(tcell.KeyTAB, '/', 0)
assert.NotEmpty(t, app.keyboard(evt2))
assert.Equal(t, app.keyboard(evt2), evt2)
})
t.Run("help view", func(t *testing.T) {
assert.Empty(t, app.helpView(nil))
assert.Equal(t, app.content.IsLastComponent(), false)
assert.Empty(t, app.helpView(nil))
assert.Equal(t, app.content.IsLastComponent(), true)
})
t.Run("back", func(t *testing.T) {
assert.Empty(t, app.helpView(nil))
app.Back(nil)
assert.Equal(t, app.content.IsLastComponent(), true)
})
}
+115
View File
@@ -0,0 +1,115 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/references/cli/top/component"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// ApplicationView is the application view, this view display info of application of KubeVela
type ApplicationView struct {
*ResourceView
ctx context.Context
}
// NewApplicationView return a new application view
func NewApplicationView(ctx context.Context, app *App) model.Component {
v := &ApplicationView{
ResourceView: NewResourceView(app),
ctx: ctx,
}
return v
}
// Init the application view
func (v *ApplicationView) Init() {
// set title of view
title := fmt.Sprintf("[ %s ]", v.Name())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
// init view
resourceList := v.ListApplications()
v.ResourceView.Init(resourceList)
v.ColorizeStatusText(len(resourceList.Body()))
v.bindKeys()
}
// ListApplications list all applications
func (v *ApplicationView) ListApplications() model.ResourceList {
return model.ListApplications(v.ctx, v.app.client)
}
// ColorizeStatusText colorize the status column text
func (v *ApplicationView) ColorizeStatusText(rowNum int) {
for i := 1; i < rowNum+1; i++ {
status := v.Table.GetCell(i, 2).Text
switch common.ApplicationPhase(status) {
case common.ApplicationRendering, common.ApplicationStarting:
status = config.ApplicationStartingAndRenderingPhaseColor + status
case common.ApplicationWorkflowSuspending:
status = config.ApplicationWorkflowSuspendingPhaseColor + status
case common.ApplicationWorkflowTerminated:
status = config.ApplicationWorkflowTerminatedPhaseColor + status
case common.ApplicationRunning:
status = config.ApplicationRunningPhaseColor + status
default:
}
v.Table.GetCell(i, 2).SetText(status)
}
}
// Name return application view name
func (v *ApplicationView) Name() string {
return "Application"
}
// Hint return key action menu hints of the application view
func (v *ApplicationView) Hint() []model.MenuHint {
return v.Actions().Hint()
}
func (v *ApplicationView) bindKeys() {
v.Actions().Delete([]tcell.Key{tcell.KeyEnter})
v.Actions().Add(model.KeyActions{
tcell.KeyEnter: model.KeyAction{Description: "Goto", Action: v.clusterView, Visible: true, Shared: true},
tcell.KeyESC: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true},
component.KeyHelp: model.KeyAction{Description: "Help", Action: v.app.helpView, Visible: true, Shared: true},
})
}
// clusterView switch app main view to the cluster view
func (v *ApplicationView) clusterView(event *tcell.EventKey) *tcell.EventKey {
row, _ := v.GetSelection()
if row == 0 {
return event
}
name, namespace := v.GetCell(row, 0).Text, v.GetCell(row, 1).Text
v.ctx = context.WithValue(v.ctx, &model.CtxKeyAppName, name)
v.ctx = context.WithValue(v.ctx, &model.CtxKeyNamespace, namespace)
v.app.command.run(v.ctx, "cluster")
return event
}
@@ -0,0 +1,81 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"testing"
"time"
"github.com/rivo/tview"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
func TestApplicationView(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
assert.Equal(t, len(app.Components), 4)
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyNamespace, "")
view := NewApplicationView(ctx, app)
appView, ok := (view).(*ApplicationView)
assert.Equal(t, ok, true)
t.Run("init", func(t *testing.T) {
appView.Init()
assert.Equal(t, appView.Table.GetTitle(), "[ Application ]")
assert.Equal(t, appView.GetCell(0, 0).Text, "Name")
})
t.Run("colorize text", func(t *testing.T) {
testData := [][]string{{"app", "ns", "running", ""}, {"app", "ns", "workflowSuspending", ""}, {"app", "ns", "workflowTerminated", ""}, {"app", "ns", "rendering", ""}}
for i := 0; i < len(testData); i++ {
for j := 0; j < 4; j++ {
appView.Table.SetCell(1+i, j, tview.NewTableCell(testData[i][j]))
}
}
appView.ColorizeStatusText(4)
assert.Equal(t, appView.GetCell(1, 2).Text, "[green::]running")
assert.Equal(t, appView.GetCell(2, 2).Text, "[yellow::]workflowSuspending")
assert.Equal(t, appView.GetCell(3, 2).Text, "[red::]workflowTerminated")
assert.Equal(t, appView.GetCell(4, 2).Text, "[blue::]rendering")
})
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(appView.Hint()), 3)
})
t.Run("cluster view", func(t *testing.T) {
appView.Table.Table.Table = appView.Table.Select(1, 1)
assert.Empty(t, appView.clusterView(nil))
})
}
+90
View File
@@ -0,0 +1,90 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/oam-dev/kubevela/references/cli/top/component"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// ClusterView is the cluster view, this view display info of cluster where selected application deployed
type ClusterView struct {
*ResourceView
ctx context.Context
}
// NewClusterView return a new cluster view
func NewClusterView(ctx context.Context, app *App) model.Component {
v := &ClusterView{
ResourceView: NewResourceView(app),
ctx: ctx,
}
return v
}
// Init cluster view init
func (v *ClusterView) Init() {
// set title of view
title := fmt.Sprintf("[ %s ]", v.Name())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
resourceList := v.ListClusters()
v.ResourceView.Init(resourceList)
v.bindKeys()
}
// ListClusters list clusters where application deployed
func (v *ClusterView) ListClusters() model.ResourceList {
return model.ListClusters(v.ctx, v.app.client)
}
// Name return cluster view name
func (v *ClusterView) Name() string {
return "Cluster"
}
// Hint return key action menu hints of the cluster view
func (v *ClusterView) Hint() []model.MenuHint {
return v.Actions().Hint()
}
func (v *ClusterView) bindKeys() {
v.Actions().Delete([]tcell.Key{tcell.KeyEnter})
v.Actions().Add(model.KeyActions{
tcell.KeyEnter: model.KeyAction{Description: "Goto", Action: v.k8sObjectView, Visible: true, Shared: true},
tcell.KeyESC: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true},
component.KeyHelp: model.KeyAction{Description: "Help", Action: v.app.helpView, Visible: true, Shared: true},
})
}
// k8sObjectView switch app main view to k8s object view
func (v *ClusterView) k8sObjectView(event *tcell.EventKey) *tcell.EventKey {
row, _ := v.GetSelection()
if row == 0 {
return event
}
clusterName := v.GetCell(row, 0).Text
v.ctx = context.WithValue(v.ctx, &model.CtxKeyCluster, clusterName)
v.app.command.run(v.ctx, "k8s")
return event
}
@@ -0,0 +1,72 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"testing"
"time"
"github.com/rivo/tview"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
func TestClusterView(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
assert.Equal(t, len(app.Components), 4)
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyAppName, "")
ctx = context.WithValue(ctx, &model.CtxKeyNamespace, "")
view := NewClusterView(ctx, app)
clusterView, ok := (view).(*ClusterView)
assert.Equal(t, ok, true)
t.Run("init", func(t *testing.T) {
clusterView.Init()
assert.Equal(t, clusterView.Table.GetTitle(), "[ Cluster ]")
assert.Equal(t, clusterView.GetCell(0, 0).Text, "Name")
})
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(clusterView.Hint()), 3)
})
t.Run("k8sObjectView", func(t *testing.T) {
testData := []string{"local", "", "", "", ""}
for j := 0; j < 5; j++ {
clusterView.Table.SetCell(1, j, tview.NewTableCell(testData[j]))
}
assert.Equal(t, clusterView.GetCell(1, 0).Text, "local")
clusterView.Table.Table.Table = clusterView.Table.Select(1, 1)
assert.Empty(t, clusterView.k8sObjectView(nil))
})
}
+60
View File
@@ -0,0 +1,60 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"strings"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// Command is a kind of abstract of user UI operation
type Command struct {
app *App
}
// NewCommand return app's command instance
func NewCommand(app *App) *Command {
command := &Command{
app: app,
}
return command
}
// Init command instance
func (c *Command) Init() {}
func (c *Command) exec(_ string, component model.Component) {
c.app.inject(component)
}
func (c *Command) run(ctx context.Context, cmd string) {
cmd = strings.ToLower(cmd)
var component model.Component
switch {
case cmd == "?" || cmd == "h" || cmd == "help":
component = NewHelpView(c.app)
default:
if resource, ok := ResourceMap[cmd]; ok {
component = resource.viewFunc(ctx, c.app)
} else {
return
}
}
c.exec(cmd, component)
}
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/oam-dev/kubevela/references/cli/top/component"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// HelpView is the view which display help tips about how to use app
type HelpView struct {
*Table
app *App
}
// NewHelpView return a new help view
func NewHelpView(app *App) *HelpView {
v := &HelpView{
Table: NewTable(),
app: app,
}
return v
}
// Init help view init
func (v *HelpView) Init() {
v.Table.Init()
title := fmt.Sprintf("[ %s ]", v.Name())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
v.bindKeys()
}
func (v *HelpView) bindKeys() {
v.Actions().Add(model.KeyActions{
tcell.KeyESC: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true},
component.KeyHelp: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true},
})
}
// Name return help view name
func (v *HelpView) Name() string {
return "Help"
}
+49
View File
@@ -0,0 +1,49 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
func TestHelpView(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
assert.Equal(t, len(app.Components), 4)
helpView := NewHelpView(app)
t.Run("init", func(t *testing.T) {
helpView.Init()
assert.Equal(t, helpView.GetTitle(), "[ Help ]")
})
}
+99
View File
@@ -0,0 +1,99 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"fmt"
"github.com/gdamore/tcell/v2"
querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types"
"github.com/oam-dev/kubevela/references/cli/top/component"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// K8SView is a view which displays info of kubernetes objects which are generated when application deployed
type K8SView struct {
*ResourceView
ctx context.Context
}
// NewK8SView return a new k8s view
func NewK8SView(ctx context.Context, app *App) model.Component {
v := &K8SView{
ResourceView: NewResourceView(app),
ctx: ctx,
}
return v
}
// Init k8s view
func (v *K8SView) Init() {
// set title of view
title := fmt.Sprintf("[ %s ]", v.Name())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
resourceList := v.ListK8SObjects()
v.ResourceView.Init(resourceList)
v.ColorizeStatusText(len(resourceList.Body()))
v.bindKeys()
}
// ListK8SObjects return kubernetes objects of the aimed application
func (v *K8SView) ListK8SObjects() model.ResourceList {
return model.ListObjects(v.ctx, v.app.client)
}
// Name return k8s view name
func (v *K8SView) Name() string {
return "K8S-Object"
}
// Hint return key action menu hints of the k8s view
func (v *K8SView) Hint() []model.MenuHint {
return v.Actions().Hint()
}
// ColorizeStatusText colorize the status column text
func (v *K8SView) ColorizeStatusText(rowNum int) {
for i := 1; i < rowNum+1; i++ {
status := v.Table.GetCell(i, 5).Text
switch querytypes.HealthStatusCode(status) {
case querytypes.HealthStatusHealthy:
status = config.ObjectHealthyStatusColor + status
case querytypes.HealthStatusUnHealthy:
status = config.ObjectUnhealthyStatusColor + status
case querytypes.HealthStatusProgressing:
status = config.ObjectProgressingStatusColor + status
case querytypes.HealthStatusUnKnown:
status = config.ObjectUnKnownStatusColor + status
default:
}
v.Table.GetCell(i, 5).SetText(status)
}
}
func (v *K8SView) bindKeys() {
v.Actions().Delete([]tcell.Key{tcell.KeyEnter})
v.Actions().Add(model.KeyActions{
tcell.KeyESC: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true},
component.KeyHelp: model.KeyAction{Description: "Help", Action: v.app.helpView, Visible: true, Shared: true},
})
}
+85
View File
@@ -0,0 +1,85 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"testing"
"time"
"github.com/rivo/tview"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
func TestK8SView(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
assert.Equal(t, len(app.Components), 4)
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyAppName, "")
ctx = context.WithValue(ctx, &model.CtxKeyNamespace, "")
ctx = context.WithValue(ctx, &model.CtxKeyCluster, "")
view := NewK8SView(ctx, app)
k8sView, ok := (view).(*K8SView)
assert.Equal(t, ok, true)
t.Run("init", func(t *testing.T) {
k8sView.Init()
assert.Equal(t, k8sView.Table.GetTitle(), "[ K8S-Object ]")
assert.Equal(t, k8sView.GetCell(0, 0).Text, "Name")
})
t.Run("colorize text", func(t *testing.T) {
testData := [][]string{
{"app", "ns", "", "", "", "Healthy"},
{"app", "ns", "", "", "", "UnHealthy"},
{"app", "ns", "", "", "", "Progressing"},
{"app", "ns", "", "", "", "UnKnown"}}
for i := 0; i < len(testData); i++ {
for j := 0; j < len(testData[i]); j++ {
k8sView.Table.SetCell(1+i, j, tview.NewTableCell(testData[i][j]))
}
}
k8sView.ColorizeStatusText(4)
assert.Equal(t, k8sView.GetCell(1, 5).Text, "[green::]Healthy")
assert.Equal(t, k8sView.GetCell(2, 5).Text, "[red::]UnHealthy")
assert.Equal(t, k8sView.GetCell(3, 5).Text, "[blue::]Progressing")
assert.Equal(t, k8sView.GetCell(4, 5).Text, "[gray::]UnKnown")
})
t.Run("hint", func(t *testing.T) {
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(k8sView.Hint()), 2)
})
})
}
+51
View File
@@ -0,0 +1,51 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"github.com/oam-dev/kubevela/references/cli/top/component"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// PageStack store views of app's main view, and it's a high level packing of "component.Pages"
type PageStack struct {
*component.Pages
app *App
}
// NewPageStack returns a new page stack.
func NewPageStack(app *App) *PageStack {
ps := &PageStack{
Pages: component.NewPages(),
app: app,
}
ps.Init()
return ps
}
// Init the pageStack
func (ps *PageStack) Init() {
ps.Stack.AddListener(ps)
}
// StackPop change itself when accept "pop" notify from app's main view
func (ps *PageStack) StackPop(_, _ model.Component) {}
// StackPush change itself when accept "pop" notify from app's main view
func (ps *PageStack) StackPush(component model.Component) {
ps.app.SetFocus(component)
}
@@ -0,0 +1,54 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
func TestPageStack(t *testing.T) {
testEnv := &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
}
cfg, err := testEnv.Start()
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
assert.Equal(t, len(app.Components), 4)
stack := NewPageStack(app)
stack.Init()
t.Run("stack push", func(t *testing.T) {
helpView := NewHelpView(app)
stack.StackPush(helpView)
})
t.Run("stack pop", func(t *testing.T) {
stack.StackPop(nil, nil)
})
}
+97
View File
@@ -0,0 +1,97 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"context"
"github.com/rivo/tview"
"github.com/oam-dev/kubevela/references/cli/top/config"
"github.com/oam-dev/kubevela/references/cli/top/model"
)
// ResourceView is an abstract of resource view
type ResourceView struct {
*Table
app *App
}
// ResourceViewer is resource's renderer
type ResourceViewer struct {
viewFunc func(context.Context, *App) model.Component
}
// ResourceMap is a map from resource name to resource's renderer
var ResourceMap = map[string]ResourceViewer{
"app": {
viewFunc: NewApplicationView,
},
"cluster": {
viewFunc: NewClusterView,
},
"k8s": {
viewFunc: NewK8SView,
},
}
// NewResourceView return a new resource view
func NewResourceView(app *App) *ResourceView {
v := &ResourceView{
Table: NewTable(),
app: app,
}
return v
}
// Init the resource view
func (v *ResourceView) Init(list model.ResourceList) {
v.SetSelectable(true, false)
v.buildTable(list)
}
func (v *ResourceView) buildTable(list model.ResourceList) {
v.Table.Init()
v.buildTableHeader(list.Header())
v.buildTableBody(list.Body())
}
// buildTableHeader render the resource table header
func (v *ResourceView) buildTableHeader(header []string) {
for i := 0; i < len(header); i++ {
c := tview.NewTableCell(header[i]).SetTextColor(config.ResourceTableHeaderColor)
c.SetExpansion(3)
v.SetCell(0, i, c)
}
}
// buildTableBody render the resource table body
func (v *ResourceView) buildTableBody(body [][]string) {
for i := 0; i < len(body); i++ {
for j := 0; j < len(body[i]); j++ {
c := tview.NewTableCell(body[i][j])
c.SetTextColor(config.ResourceTableBodyColor)
c.SetExpansion(3)
v.SetCell(i+1, j, c)
}
}
}
// Name return view name
func (v *ResourceView) Name() string {
return "Resource"
}
@@ -0,0 +1,28 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestResourceView(t *testing.T) {
view := NewResourceView(nil)
assert.Equal(t, view.Name(), "Resource")
}
+53
View File
@@ -0,0 +1,53 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"github.com/gdamore/tcell/v2"
"github.com/oam-dev/kubevela/references/cli/top/component"
)
// Table is a base table view, help view and resource view all base on it
type Table struct {
*component.Table
}
// NewTable return a new table view
func NewTable() *Table {
t := &Table{
Table: component.NewTable(),
}
return t
}
// Init the table view
func (t *Table) Init() {
t.Table.Init()
t.SetInputCapture(t.keyboard)
}
func (t *Table) keyboard(event *tcell.EventKey) *tcell.EventKey {
key := event.Key()
if key == tcell.KeyUp || key == tcell.KeyDown {
return event
}
if a, ok := t.Actions()[tcell.Key(event.Rune())]; ok {
return a.Action(event)
}
return event
}
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package view
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
)
func TestTable(t *testing.T) {
table := NewTable()
t.Run("init", func(t *testing.T) {
table.Init()
assert.Equal(t, table.Table.GetBorderAttributes(), tcell.AttrItalic)
})
t.Run("keyboard", func(t *testing.T) {
evt1 := tcell.NewEventKey(tcell.KeyUp, '/', 0)
assert.Equal(t, table.keyboard(evt1), evt1)
evt2 := tcell.NewEventKey(tcell.KeyEsc, '/', 0)
assert.Equal(t, table.keyboard(evt2), evt2)
})
}