diff --git a/go.mod b/go.mod index 9313acb64..04af35afa 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c8da85d23..b9d564980 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index c22c632de..402b2a00c 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -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 diff --git a/pkg/velaql/providers/query/tree.go b/pkg/velaql/providers/query/tree.go index cd078ccd0..71b54e880 100644 --- a/pkg/velaql/providers/query/tree.go +++ b/pkg/velaql/providers/query/tree.go @@ -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 } diff --git a/pkg/velaql/providers/query/tree_test.go b/pkg/velaql/providers/query/tree_test.go index 6684d8780..473e749a1 100644 --- a/pkg/velaql/providers/query/tree_test.go +++ b/pkg/velaql/providers/query/tree_test.go @@ -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) } diff --git a/references/cli/cli.go b/references/cli/cli.go index 3e2572825..af1dcd8e7 100644 --- a/references/cli/cli.go +++ b/references/cli/cli.go @@ -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), diff --git a/references/cli/top.go b/references/cli/top.go new file mode 100644 index 000000000..0efe3d99b --- /dev/null +++ b/references/cli/top.go @@ -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() +} diff --git a/references/cli/top/component/app.go b/references/cli/top/component/app.go new file mode 100644 index 000000000..5d3ff1f8c --- /dev/null +++ b/references/cli/top/component/app.go @@ -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) +} diff --git a/references/cli/top/component/app_test.go b/references/cli/top/component/app_test.go new file mode 100644 index 000000000..ec98468ee --- /dev/null +++ b/references/cli/top/component/app_test.go @@ -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()) + }) +} diff --git a/references/cli/top/component/crumbs.go b/references/cli/top/component/crumbs.go new file mode 100644 index 000000000..c9fe0da4b --- /dev/null +++ b/references/cli/top/component/crumbs.go @@ -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) +} diff --git a/references/cli/top/component/crumbs_test.go b/references/cli/top/component/crumbs_test.go new file mode 100644 index 000000000..fb8104e89 --- /dev/null +++ b/references/cli/top/component/crumbs_test.go @@ -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) + }) +} diff --git a/references/cli/top/component/info.go b/references/cli/top/component/info.go new file mode 100644 index 000000000..71d9e34f1 --- /dev/null +++ b/references/cli/top/component/info.go @@ -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 +} diff --git a/references/cli/top/component/info_test.go b/references/cli/top/component/info_test.go new file mode 100644 index 000000000..516351419 --- /dev/null +++ b/references/cli/top/component/info_test.go @@ -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:") +} diff --git a/references/cli/top/component/key.go b/references/cli/top/component/key.go new file mode 100644 index 000000000..383f13afc --- /dev/null +++ b/references/cli/top/component/key.go @@ -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() +} diff --git a/references/cli/top/component/logo.go b/references/cli/top/component/logo.go new file mode 100644 index 000000000..398c5a811 --- /dev/null +++ b/references/cli/top/component/logo.go @@ -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) + } +} diff --git a/references/cli/top/component/logo_test.go b/references/cli/top/component/logo_test.go new file mode 100644 index 000000000..03d83b1d4 --- /dev/null +++ b/references/cli/top/component/logo_test.go @@ -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 /| __/| || (_| | +|_|\_\\__,_||_.__/ \___| \_/ \___||_| \__,_| + +`) +} diff --git a/references/cli/top/component/menu.go b/references/cli/top/component/menu.go new file mode 100644 index 000000000..3eaf3b52d --- /dev/null +++ b/references/cli/top/component/menu.go @@ -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) +} diff --git a/references/cli/top/component/menu_test.go b/references/cli/top/component/menu_test.go new file mode 100644 index 000000000..48bb331c9 --- /dev/null +++ b/references/cli/top/component/menu_test.go @@ -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] [:-: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] [:-:b] ") + }) +} diff --git a/references/cli/top/component/pages.go b/references/cli/top/component/pages.go new file mode 100644 index 000000000..b3c1ca677 --- /dev/null +++ b/references/cli/top/component/pages.go @@ -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) +} diff --git a/references/cli/top/component/pages_test.go b/references/cli/top/component/pages_test.go new file mode 100644 index 000000000..99fda10f7 --- /dev/null +++ b/references/cli/top/component/pages_test.go @@ -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) + }) +} diff --git a/references/cli/top/component/table.go b/references/cli/top/component/table.go new file mode 100644 index 000000000..8f8c5d4f2 --- /dev/null +++ b/references/cli/top/component/table.go @@ -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 +} diff --git a/references/cli/top/component/table_test.go b/references/cli/top/component/table_test.go new file mode 100644 index 000000000..daf445ac1 --- /dev/null +++ b/references/cli/top/component/table_test.go @@ -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: "", + }) + }) +} diff --git a/references/cli/top/config/color.go b/references/cli/top/config/color.go new file mode 100644 index 000000000..a144bd105 --- /dev/null +++ b/references/cli/top/config/color.go @@ -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::]" +) diff --git a/references/cli/top/config/config.go b/references/cli/top/config/config.go new file mode 100644 index 000000000..7a0855fe6 --- /dev/null +++ b/references/cli/top/config/config.go @@ -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 +} diff --git a/references/cli/top/config/size.go b/references/cli/top/config/size.go new file mode 100644 index 000000000..2e1d14540 --- /dev/null +++ b/references/cli/top/config/size.go @@ -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 +) diff --git a/references/cli/top/model/application.go b/references/cli/top/model/application.go new file mode 100644 index 000000000..9ded3b3c9 --- /dev/null +++ b/references/cli/top/model/application.go @@ -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 +} diff --git a/references/cli/top/model/application_test.go b/references/cli/top/model/application_test.go new file mode 100644 index 000000000..4b6a59800 --- /dev/null +++ b/references/cli/top/model/application_test.go @@ -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)) + }) +}) diff --git a/references/cli/top/model/cluster.go b/references/cli/top/model/cluster.go new file mode 100644 index 000000000..68bd9744e --- /dev/null +++ b/references/cli/top/model/cluster.go @@ -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 +} diff --git a/references/cli/top/model/cluster_test.go b/references/cli/top/model/cluster_test.go new file mode 100644 index 000000000..c8bb28d76 --- /dev/null +++ b/references/cli/top/model/cluster_test.go @@ -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", "-", ""})) + }) +}) diff --git a/references/cli/top/model/info.go b/references/cli/top/model/info.go new file mode 100644 index 000000000..97dca56cb --- /dev/null +++ b/references/cli/top/model/info.go @@ -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() +} diff --git a/references/cli/top/model/info_test.go b/references/cli/top/model/info_test.go new file mode 100644 index 000000000..617bb2e17 --- /dev/null +++ b/references/cli/top/model/info_test.go @@ -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()) + }) +}) diff --git a/references/cli/top/model/k8s_object.go b/references/cli/top/model/k8s_object.go new file mode 100644 index 000000000..bed185b9a --- /dev/null +++ b/references/cli/top/model/k8s_object.go @@ -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 +} diff --git a/references/cli/top/model/k8s_object_test.go b/references/cli/top/model/k8s_object_test.go new file mode 100644 index 000000000..41052fa43 --- /dev/null +++ b/references/cli/top/model/k8s_object_test.go @@ -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)) + }) +}) diff --git a/references/cli/top/model/key_actions.go b/references/cli/top/model/key_actions.go new file mode 100644 index 000000000..8b6462ada --- /dev/null +++ b/references/cli/top/model/key_actions.go @@ -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) + } +} diff --git a/references/cli/top/model/key_actions_test.go b/references/cli/top/model/key_actions_test.go new file mode 100644 index 000000000..02d33b135 --- /dev/null +++ b/references/cli/top/model/key_actions_test.go @@ -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) +} diff --git a/references/cli/top/model/menu_hint.go b/references/cli/top/model/menu_hint.go new file mode 100644 index 000000000..8da7ea748 --- /dev/null +++ b/references/cli/top/model/menu_hint.go @@ -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 +} diff --git a/references/cli/top/model/resource.go b/references/cli/top/model/resource.go new file mode 100644 index 000000000..f487a21c5 --- /dev/null +++ b/references/cli/top/model/resource.go @@ -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 +} diff --git a/references/cli/top/model/stack.go b/references/cli/top/model/stack.go new file mode 100644 index 000000000..4f234d776 --- /dev/null +++ b/references/cli/top/model/stack.go @@ -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()) + } + } +} diff --git a/references/cli/top/model/stack_test.go b/references/cli/top/model/stack_test.go new file mode 100644 index 000000000..d83314cdf --- /dev/null +++ b/references/cli/top/model/stack_test.go @@ -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) +} diff --git a/references/cli/top/model/suit_test.go b/references/cli/top/model/suit_test.go new file mode 100644 index 000000000..94693ca43 --- /dev/null +++ b/references/cli/top/model/suit_test.go @@ -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") +} diff --git a/references/cli/top/model/type.go b/references/cli/top/model/type.go new file mode 100644 index 000000000..c3ca5a198 --- /dev/null +++ b/references/cli/top/model/type.go @@ -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" +) diff --git a/references/cli/top/view/app.go b/references/cli/top/view/app.go new file mode 100644 index 000000000..128c9cca1 --- /dev/null +++ b/references/cli/top/view/app.go @@ -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 +} diff --git a/references/cli/top/view/app_test.go b/references/cli/top/view/app_test.go new file mode 100644 index 000000000..8258f6a69 --- /dev/null +++ b/references/cli/top/view/app_test.go @@ -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) + }) +} diff --git a/references/cli/top/view/application_view.go b/references/cli/top/view/application_view.go new file mode 100644 index 000000000..3c11b09d4 --- /dev/null +++ b/references/cli/top/view/application_view.go @@ -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 +} diff --git a/references/cli/top/view/application_view_test.go b/references/cli/top/view/application_view_test.go new file mode 100644 index 000000000..76bc23d86 --- /dev/null +++ b/references/cli/top/view/application_view_test.go @@ -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)) + }) + +} diff --git a/references/cli/top/view/cluster_view.go b/references/cli/top/view/cluster_view.go new file mode 100644 index 000000000..6142308ae --- /dev/null +++ b/references/cli/top/view/cluster_view.go @@ -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 +} diff --git a/references/cli/top/view/cluster_view_test.go b/references/cli/top/view/cluster_view_test.go new file mode 100644 index 000000000..84502c11b --- /dev/null +++ b/references/cli/top/view/cluster_view_test.go @@ -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)) + }) +} diff --git a/references/cli/top/view/command.go b/references/cli/top/view/command.go new file mode 100644 index 000000000..4cabf873b --- /dev/null +++ b/references/cli/top/view/command.go @@ -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) +} diff --git a/references/cli/top/view/help_view.go b/references/cli/top/view/help_view.go new file mode 100644 index 000000000..ff151c76a --- /dev/null +++ b/references/cli/top/view/help_view.go @@ -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" +} diff --git a/references/cli/top/view/help_view_test.go b/references/cli/top/view/help_view_test.go new file mode 100644 index 000000000..c7e3763cd --- /dev/null +++ b/references/cli/top/view/help_view_test.go @@ -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 ]") + }) +} diff --git a/references/cli/top/view/k8s_view.go b/references/cli/top/view/k8s_view.go new file mode 100644 index 000000000..ff2a1dea5 --- /dev/null +++ b/references/cli/top/view/k8s_view.go @@ -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}, + }) +} diff --git a/references/cli/top/view/k8s_view_test.go b/references/cli/top/view/k8s_view_test.go new file mode 100644 index 000000000..e71468168 --- /dev/null +++ b/references/cli/top/view/k8s_view_test.go @@ -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) + }) + }) + +} diff --git a/references/cli/top/view/page_stack.go b/references/cli/top/view/page_stack.go new file mode 100644 index 000000000..1e6fd4cef --- /dev/null +++ b/references/cli/top/view/page_stack.go @@ -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) +} diff --git a/references/cli/top/view/page_stack_test.go b/references/cli/top/view/page_stack_test.go new file mode 100644 index 000000000..8f77c8731 --- /dev/null +++ b/references/cli/top/view/page_stack_test.go @@ -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) + }) +} diff --git a/references/cli/top/view/resource_view.go b/references/cli/top/view/resource_view.go new file mode 100644 index 000000000..e6e8fd6d5 --- /dev/null +++ b/references/cli/top/view/resource_view.go @@ -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" +} diff --git a/references/cli/top/view/resource_view_test.go b/references/cli/top/view/resource_view_test.go new file mode 100644 index 000000000..0a2aec633 --- /dev/null +++ b/references/cli/top/view/resource_view_test.go @@ -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") +} diff --git a/references/cli/top/view/table.go b/references/cli/top/view/table.go new file mode 100644 index 000000000..dab3fc618 --- /dev/null +++ b/references/cli/top/view/table.go @@ -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 +} diff --git a/references/cli/top/view/table_test.go b/references/cli/top/view/table_test.go new file mode 100644 index 000000000..109eb62cb --- /dev/null +++ b/references/cli/top/view/table_test.go @@ -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) + }) +}