Feat: add filtering features to the Application view and K8S object view of vela top (#4612)

* Feat: when `vela top` launch, can specify the namespace of the presentation application

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

* Feat: add filtering function to the k8s object view of the `vela top` command, which can be filtered by cluster and cluster namespace

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

Signed-off-by: HanMengnan <1448189829@qq.com>
This commit is contained in:
Siege Lion
2022-08-15 10:36:12 +08:00
committed by GitHub
parent 72591788a6
commit 2e18eaa3b2
26 changed files with 749 additions and 39 deletions

View File

@@ -34,19 +34,35 @@ func NewTopCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *co
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`,
vela top
# Show applications which are in <vela-namespace> namespace
vela top -n <vela-namespace>
# Show applications which are in all namespaces
vela top -A
`,
RunE: func(cmd *cobra.Command, args []string) error {
return launchUI(c, cmd)
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
if err != nil {
return err
}
if AllNamespace {
namespace = ""
}
return launchUI(c, namespace)
},
Annotations: map[string]string{
types.TagCommandOrder: order,
types.TagCommandType: types.TypeApp,
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().BoolVarP(&AllNamespace, "all-namespaces", "A", false, "If true, check the specified action in all namespaces.")
return cmd
}
func launchUI(c common.Args, _ *cobra.Command) error {
func launchUI(c common.Args, namespace string) error {
k8sClient, err := c.GetClient()
if err != nil {
return fmt.Errorf("cannot get k8s client: %w", err)
@@ -55,7 +71,7 @@ func launchUI(c common.Args, _ *cobra.Command) error {
if err != nil {
return err
}
app := view.NewApp(k8sClient, restConfig)
app := view.NewApp(k8sClient, restConfig, namespace)
app.Init()
return app.Run()

View File

@@ -29,10 +29,71 @@ const (
KeySpace = 32
)
// Defines char keystrokes.
const (
KeyA tcell.Key = iota + 97
KeyB
KeyC
KeyD
KeyE
KeyF
KeyG
KeyH
KeyI
KeyJ
KeyK
KeyL
KeyM
KeyN
KeyO
KeyP
KeyQ
KeyR
KeyS
KeyT
KeyU
KeyV
KeyW
KeyX
KeyY
KeyZ
)
func init() {
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
tcell.KeyNames[tcell.Key(KeySpace)] = "space"
initStdKeys()
}
func initStdKeys() {
tcell.KeyNames[KeyA] = "a"
tcell.KeyNames[KeyB] = "b"
tcell.KeyNames[KeyC] = "c"
tcell.KeyNames[KeyD] = "d"
tcell.KeyNames[KeyE] = "e"
tcell.KeyNames[KeyF] = "f"
tcell.KeyNames[KeyG] = "g"
tcell.KeyNames[KeyH] = "h"
tcell.KeyNames[KeyI] = "i"
tcell.KeyNames[KeyJ] = "j"
tcell.KeyNames[KeyK] = "k"
tcell.KeyNames[KeyL] = "l"
tcell.KeyNames[KeyM] = "m"
tcell.KeyNames[KeyN] = "n"
tcell.KeyNames[KeyO] = "o"
tcell.KeyNames[KeyP] = "p"
tcell.KeyNames[KeyQ] = "q"
tcell.KeyNames[KeyR] = "r"
tcell.KeyNames[KeyS] = "s"
tcell.KeyNames[KeyT] = "t"
tcell.KeyNames[KeyU] = "u"
tcell.KeyNames[KeyV] = "v"
tcell.KeyNames[KeyW] = "w"
tcell.KeyNames[KeyX] = "x"
tcell.KeyNames[KeyY] = "y"
tcell.KeyNames[KeyZ] = "z"
}
// StandardizeKey standardized combined key event and return corresponding key

View File

@@ -42,7 +42,12 @@ func NewMenu() *Menu {
// StackPop change itself when accept "pop" notify from app's main view
func (m *Menu) StackPop(old, new model.Component) {
m.UpdateMenu(new.Hint())
if new == nil {
m.UpdateMenu([]model.MenuHint{})
} else {
m.UpdateMenu(new.Hint())
}
}
// StackPush change itself when accept "push" notify from app's main view

View File

@@ -60,6 +60,7 @@ func (l *ClusterList) Body() [][]string {
func ListClusters(ctx context.Context, c client.Client) *ClusterList {
list := &ClusterList{
title: []string{"Name", "Alias", "Type", "EndPoint", "Labels"},
data: []Cluster{{"all", "*", "*", "*", "*"}},
}
name := ctx.Value(&CtxKeyAppName).(string)
ns := ctx.Value(&CtxKeyNamespace).(string)

View File

@@ -0,0 +1,68 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// ListClusterNamespaces return namespace of application's resource
func ListClusterNamespaces(ctx context.Context, c client.Client) ResourceList {
list := &NamespaceList{
title: []string{"Name", "Status", "Age"},
data: []Namespace{{"all", "*", "*"}},
}
name := ctx.Value(&CtxKeyAppName).(string)
ns := ctx.Value(&CtxKeyNamespace).(string)
app, err := LoadApplication(c, name, ns)
if err != nil {
return list
}
clusterNSSet := make(map[string]interface{})
for _, svc := range app.Status.AppliedResources {
if svc.Namespace != "" {
clusterNSSet[svc.Namespace] = struct{}{}
}
}
for clusterNS := range clusterNSSet {
namespaceInfo := LoadNamespaceDetail(ctx, c, clusterNS)
if namespaceInfo != nil {
list.data = append(list.data, Namespace{
Name: namespaceInfo.Name,
Status: string(namespaceInfo.Status.Phase),
Age: timeFormat(time.Since(namespaceInfo.CreationTimestamp.Time)),
})
}
}
return list
}
// LoadNamespaceDetail query detail info of a namespace by name
func LoadNamespaceDetail(ctx context.Context, c client.Client, namespace string) *v1.Namespace {
ns := new(v1.Namespace)
if err := c.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
return nil
}
return ns
}

View File

@@ -0,0 +1,42 @@
/*
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"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("test cluster namespace", func() {
ctx := context.Background()
ctx = context.WithValue(ctx, &CtxKeyAppName, "first-vela-app")
ctx = context.WithValue(ctx, &CtxKeyNamespace, "default")
It("list cluster namespace", func() {
cnsList := ListClusterNamespaces(ctx, k8sClient)
Expect(len(cnsList.Header())).To(Equal(3))
Expect(cnsList.Header()).To(Equal([]string{"Name", "Status", "Age"}))
Expect(len(cnsList.Body())).To(Equal(2))
Expect(cnsList.Body()[1][0]).To(Equal("default"))
})
It("load cluster namespace detail info", func() {
ns := LoadNamespaceDetail(ctx, k8sClient, "default")
Expect(string(ns.Status.Phase)).To(Equal("Active"))
})
})

View File

@@ -45,7 +45,7 @@ var _ = Describe("test cluster", 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", "-", ""}))
Expect(len(clusterList.Body())).To(Equal(2))
Expect(clusterList.Body()[1]).To(Equal([]string{"local", "", "Internal", "-", ""}))
})
})

View File

@@ -21,7 +21,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/velaql/providers/query"
)
@@ -55,6 +54,28 @@ func (l *K8SObjectList) Body() [][]string {
return data
}
// FilterCluster filter out objects that belong to the target cluster
func (l *K8SObjectList) FilterCluster(cluster string) {
data := make([]K8SObject, 0)
for _, app := range l.data {
if app.cluster == cluster {
data = append(data, K8SObject{app.name, app.namespace, app.kind, app.apiVersion, app.cluster, app.status})
}
}
l.data = data
}
// FilterClusterNamespace filter out objects that belong to the target namespace
func (l *K8SObjectList) FilterClusterNamespace(clusterNS string) {
data := make([]K8SObject, 0)
for _, app := range l.data {
if app.namespace == clusterNS {
data = append(data, K8SObject{app.name, app.namespace, app.kind, app.apiVersion, app.cluster, app.status})
}
}
l.data = data
}
// ListObjects return k8s object resource list
func ListObjects(ctx context.Context, c client.Client) *K8SObjectList {
list := &K8SObjectList{
@@ -62,15 +83,13 @@ func ListObjects(ctx context.Context, c client.Client) *K8SObjectList {
}
name := ctx.Value(&CtxKeyAppName).(string)
namespace := ctx.Value(&CtxKeyNamespace).(string)
cluster := ctx.Value(&CtxKeyCluster).(string)
opt := query.Option{
Name: name,
Namespace: namespace,
Filter: query.FilterOption{},
}
if cluster != multicluster.ClusterLocalName {
opt.Filter = query.FilterOption{Cluster: cluster}
}
collector := query.NewAppCollector(c, opt)
appResList, err := collector.CollectResourceFromApp()
@@ -82,6 +101,15 @@ func ListObjects(ctx context.Context, c client.Client) *K8SObjectList {
list.data = append(list.data, LoadObjectDetail(resource))
}
cluster, ok := ctx.Value(&CtxKeyCluster).(string)
if ok && cluster != "" {
list.FilterCluster(cluster)
}
clusterNamespace, ok := ctx.Value(&CtxKeyClusterNamespace).(string)
if ok && clusterNamespace != "" {
list.FilterClusterNamespace(clusterNamespace)
}
return list
}

View File

@@ -0,0 +1,92 @@
/*
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"
"fmt"
"strconv"
"strings"
"time"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Namespace is namespace struct
type Namespace struct {
Name string
Status string
Age string
}
// NamespaceList is namespace list
type NamespaceList struct {
title []string
data []Namespace
}
// AllNamespace is the key which represents all namespaces
const AllNamespace = "all"
// ListNamespaces return all namespaces
func ListNamespaces(ctx context.Context, c client.Reader) *NamespaceList {
list := &NamespaceList{title: []string{"Name", "Status", "Age"}, data: []Namespace{{Name: AllNamespace, Status: "*", Age: "*"}}}
var nsList v1.NamespaceList
if err := c.List(ctx, &nsList); err != nil {
return list
}
for _, ns := range nsList.Items {
list.data = append(list.data, Namespace{
Name: ns.Name,
Status: string(ns.Status.Phase),
Age: timeFormat(time.Since(ns.CreationTimestamp.Time)),
})
}
return list
}
// Header generate header of table in namespace view
func (l *NamespaceList) Header() []string {
return l.title
}
// Body generate body of table in namespace view
func (l *NamespaceList) Body() [][]string {
data := make([][]string, 0)
for _, ns := range l.data {
data = append(data, []string{ns.Name, ns.Status, ns.Age})
}
return data
}
// timeFormat format time data of `time.Duration` type to string type
func timeFormat(t time.Duration) string {
str := t.String()
// remove "."
tmp := strings.Split(str, ".")
tmp[0] += "s"
tmp = strings.Split(tmp[0], "h")
// hour num
hour, err := strconv.Atoi(tmp[0])
if err != nil {
return ""
}
return fmt.Sprintf("%dd%dh%2s", hour/24, hour%24, tmp[1])
}

View File

@@ -0,0 +1,66 @@
/*
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"
"fmt"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
)
func TestNamespaceList_Header(t *testing.T) {
nsList := NamespaceList{
title: []string{"Name", "Status", "Age"},
data: []Namespace{{Name: AllNamespace, Status: "*", Age: "*"}},
}
assert.Equal(t, nsList.Header(), []string{"Name", "Status", "Age"})
}
func TestNamespaceList_Body(t *testing.T) {
nsList := NamespaceList{
title: []string{"Name", "Status", "Age"},
data: []Namespace{{Name: AllNamespace, Status: "*", Age: "*"}},
}
assert.Equal(t, len(nsList.Body()), 1)
assert.Equal(t, nsList.Body()[0], []string{AllNamespace, "*", "*"})
}
func TestTimeFormat(t *testing.T) {
t1, err1 := time.ParseDuration("1.5h")
assert.NoError(t, err1)
assert.Equal(t, timeFormat(t1), "0d1h30m0ss")
t2, err2 := time.ParseDuration("25h")
assert.NoError(t, err2)
assert.Equal(t, timeFormat(t2), "1d1h0m0ss")
}
var _ = Describe("test namespace", func() {
ctx := context.Background()
It("list namespace", func() {
nsList := ListNamespaces(ctx, k8sClient)
Expect(len(nsList.Header())).To(Equal(3))
Expect(nsList.Header()).To(Equal([]string{"Name", "Status", "Age"}))
fmt.Println(nsList.Body())
Expect(len(nsList.Body())).To(Equal(5))
Expect(nsList.Body()[0]).To(Equal([]string{"all", "*", "*"}))
})
})

View File

@@ -112,6 +112,13 @@ func (s *Stack) Empty() bool {
return len(s.components) == 0
}
// Clear out the stack
func (s *Stack) Clear() {
for !s.Empty() {
s.PopComponent()
}
}
func (s *Stack) notifyListener(action int, component Component) {
for _, listener := range s.listeners {
switch action {

View File

@@ -52,8 +52,17 @@ type (
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"
// CtxKeyCluster request context key of cluster name
CtxKeyCluster = "cluster"
// CtxKeyClusterNamespace request context key of cluster namespace name
CtxKeyClusterNamespace = "cluster"
)
const (
// AllClusterNamespace represent all cluster namespace
AllClusterNamespace = "all"
// AllCluster represent all cluster
AllCluster = "all"
)

View File

@@ -36,19 +36,22 @@ type App struct {
config config.Config
command *Command
content *PageStack
ctx context.Context
}
// NewApp return a new app object
func NewApp(c client.Client, restConfig *rest.Config) *App {
func NewApp(c client.Client, restConfig *rest.Config, namespace string) *App {
a := &App{
App: component.NewApp(),
client: c,
config: config.Config{
RestConfig: restConfig,
},
ctx: context.Background(),
}
a.command = NewCommand(a)
a.content = NewPageStack(a)
a.ctx = context.WithValue(a.ctx, &model.CtxKeyNamespace, namespace)
return a
}
@@ -125,9 +128,7 @@ func (a *App) inject(c model.Component) {
// 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")
a.command.run(a.ctx, "app")
return event
}

View File

@@ -39,7 +39,7 @@ func TestApp(t *testing.T) {
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
app := NewApp(testClient, cfg, "")
assert.Equal(t, len(app.Components), 4)
t.Run("init", func(t *testing.T) {

View File

@@ -46,7 +46,7 @@ func NewApplicationView(ctx context.Context, app *App) model.Component {
// Init the application view
func (v *ApplicationView) Init() {
// set title of view
title := fmt.Sprintf("[ %s ]", v.Name())
title := fmt.Sprintf("[ %s ]", v.Title())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
// init view
resourceList := v.ListApplications()
@@ -80,6 +80,15 @@ func (v *ApplicationView) ColorizeStatusText(rowNum int) {
}
}
// Title return table title of application view
func (v *ApplicationView) Title() string {
namespace := v.ctx.Value(&model.CtxKeyNamespace).(string)
if namespace == "" {
namespace = "all"
}
return fmt.Sprintf("Application"+" (%s)", namespace)
}
// Name return application view name
func (v *ApplicationView) Name() string {
return "Application"
@@ -93,14 +102,14 @@ func (v *ApplicationView) Hint() []model.MenuHint {
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.KeyEnter: model.KeyAction{Description: "Goto", Action: v.k8sObjectView, Visible: true, Shared: true},
component.KeyN: model.KeyAction{Description: "Select Namespace", Action: v.namespaceView, 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 {
func (v *ApplicationView) k8sObjectView(event *tcell.EventKey) *tcell.EventKey {
row, _ := v.GetSelection()
if row == 0 {
return event
@@ -110,6 +119,12 @@ func (v *ApplicationView) clusterView(event *tcell.EventKey) *tcell.EventKey {
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")
v.app.command.run(v.ctx, "k8s")
return event
}
func (v *ApplicationView) namespaceView(event *tcell.EventKey) *tcell.EventKey {
v.app.content.Clear()
v.app.command.run(v.ctx, "ns")
return event
}

View File

@@ -41,7 +41,7 @@ func TestApplicationView(t *testing.T) {
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
app := NewApp(testClient, cfg, "")
assert.Equal(t, len(app.Components), 4)
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyNamespace, "")
@@ -51,7 +51,7 @@ func TestApplicationView(t *testing.T) {
t.Run("init", func(t *testing.T) {
appView.Init()
assert.Equal(t, appView.Table.GetTitle(), "[ Application ]")
assert.Equal(t, appView.Table.GetTitle(), "[ Application (all) ]")
assert.Equal(t, appView.GetCell(0, 0).Text, "Name")
})
@@ -70,12 +70,12 @@ func TestApplicationView(t *testing.T) {
})
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(appView.Hint()), 3)
assert.Equal(t, len(appView.Hint()), 4)
})
t.Run("cluster view", func(t *testing.T) {
t.Run("object view", func(t *testing.T) {
appView.Table.Table.Table = appView.Table.Select(1, 1)
assert.Empty(t, appView.clusterView(nil))
assert.Empty(t, appView.k8sObjectView(nil))
})
}

View File

@@ -0,0 +1,94 @@
/*
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"
)
// ClusterNamespaceView is the cluster namespace, which display the namespace info of application's resource
type ClusterNamespaceView struct {
*ResourceView
ctx context.Context
}
// NewClusterNamespaceView return a new cluster namespace view
func NewClusterNamespaceView(ctx context.Context, app *App) model.Component {
v := &ClusterNamespaceView{
ResourceView: NewResourceView(app),
ctx: ctx,
}
return v
}
// Init the cluster namespace view
func (v *ClusterNamespaceView) Init() {
title := fmt.Sprintf("[ %s ]", v.Name())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
resourceList := v.ListClusterNamespaces()
v.ResourceView.Init(resourceList)
v.bindKeys()
}
// ListClusterNamespaces return the namespace of application's resource
func (v *ClusterNamespaceView) ListClusterNamespaces() model.ResourceList {
return model.ListClusterNamespaces(v.ctx, v.app.client)
}
// Hint return key action menu hints of the cluster namespace view
func (v *ClusterNamespaceView) Hint() []model.MenuHint {
return v.Actions().Hint()
}
// Name return cluster namespace view name
func (v *ClusterNamespaceView) Name() string {
return "ClusterNamespace"
}
func (v *ClusterNamespaceView) bindKeys() {
v.Actions().Delete([]tcell.Key{tcell.KeyEnter})
v.Actions().Add(model.KeyActions{
tcell.KeyEnter: model.KeyAction{Description: "Select", 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 cluster namespace view to k8s object view
func (v *ClusterNamespaceView) k8sObjectView(event *tcell.EventKey) *tcell.EventKey {
row, _ := v.GetSelection()
if row == 0 {
return event
}
v.app.content.PopComponent()
clusterNamespace := v.GetCell(row, 0).Text
if clusterNamespace == model.AllClusterNamespace {
clusterNamespace = ""
}
v.ctx = context.WithValue(v.ctx, &model.CtxKeyClusterNamespace, clusterNamespace)
v.app.command.run(v.ctx, "k8s")
return event
}

View File

@@ -0,0 +1,66 @@
/*
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/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 TestClusterNamespaceView(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, "")
cnsView, ok := NewClusterNamespaceView(ctx, app).(*ClusterNamespaceView)
assert.Equal(t, ok, true)
t.Run("init", func(t *testing.T) {
cnsView.Init()
assert.Equal(t, cnsView.GetTitle(), "[ ClusterNamespace ]")
assert.Equal(t, cnsView.GetCell(0, 0).Text, "Name")
assert.Equal(t, cnsView.GetCell(1, 0).Text, "all")
})
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(cnsView.Hint()), 3)
})
t.Run("object view", func(t *testing.T) {
cnsView.Table.Table.Table = cnsView.Table.Select(1, 1)
assert.Empty(t, cnsView.k8sObjectView(nil))
})
}

View File

@@ -77,13 +77,17 @@ func (v *ClusterView) bindKeys() {
})
}
// k8sObjectView switch app main view to k8s object view
// k8sObjectView switch cluster view to k8s object view
func (v *ClusterView) k8sObjectView(event *tcell.EventKey) *tcell.EventKey {
row, _ := v.GetSelection()
if row == 0 {
return event
}
v.app.content.PopComponent()
clusterName := v.GetCell(row, 0).Text
if clusterName == model.AllCluster {
clusterName = ""
}
v.ctx = context.WithValue(v.ctx, &model.CtxKeyCluster, clusterName)
v.app.command.run(v.ctx, "k8s")
return event

View File

@@ -41,7 +41,7 @@ func TestClusterView(t *testing.T) {
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
app := NewApp(testClient, cfg, "")
assert.Equal(t, len(app.Components), 4)
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyAppName, "")

View File

@@ -38,7 +38,7 @@ func TestHelpView(t *testing.T) {
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
app := NewApp(testClient, cfg, "")
assert.Equal(t, len(app.Components), 4)
helpView := NewHelpView(app)

View File

@@ -46,7 +46,7 @@ func NewK8SView(ctx context.Context, app *App) model.Component {
// Init k8s view
func (v *K8SView) Init() {
// set title of view
title := fmt.Sprintf("[ %s ]", v.Name())
title := fmt.Sprintf("[ %s ]", v.Title())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
resourceList := v.ListK8SObjects()
@@ -61,6 +61,19 @@ func (v *K8SView) ListK8SObjects() model.ResourceList {
return model.ListObjects(v.ctx, v.app.client)
}
// Title return the table title of k8s object view
func (v *K8SView) Title() string {
namespace, ok := v.ctx.Value(&model.CtxKeyCluster).(string)
if !ok || namespace == "" {
namespace = "all"
}
clusterNS, ok := v.ctx.Value(&model.CtxKeyClusterNamespace).(string)
if !ok || clusterNS == "" {
clusterNS = "all"
}
return fmt.Sprintf("K8S-Object"+" (%s/%s)", namespace, clusterNS)
}
// Name return k8s view name
func (v *K8SView) Name() string {
return "K8S-Object"
@@ -93,7 +106,23 @@ func (v *K8SView) ColorizeStatusText(rowNum int) {
func (v *K8SView) bindKeys() {
v.Actions().Delete([]tcell.Key{tcell.KeyEnter})
v.Actions().Add(model.KeyActions{
component.KeyC: model.KeyAction{Description: "Select Cluster", Action: v.clusterView, Visible: true, Shared: true},
component.KeyN: model.KeyAction{Description: "Select ClusterNS", Action: v.clusterNamespaceView, 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 k8s object view to the cluster view
func (v *K8SView) clusterView(event *tcell.EventKey) *tcell.EventKey {
v.app.content.PopComponent()
v.app.command.run(v.ctx, "cluster")
return event
}
// clusterView switch k8s object view to the cluster Namespace view
func (v *K8SView) clusterNamespaceView(event *tcell.EventKey) *tcell.EventKey {
v.app.content.PopComponent()
v.app.command.run(v.ctx, "cns")
return event
}

View File

@@ -41,7 +41,7 @@ func TestK8SView(t *testing.T) {
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
app := NewApp(testClient, cfg, "")
assert.Equal(t, len(app.Components), 4)
ctx := context.Background()
ctx = context.WithValue(ctx, &model.CtxKeyAppName, "")
@@ -54,7 +54,7 @@ func TestK8SView(t *testing.T) {
t.Run("init", func(t *testing.T) {
k8sView.Init()
assert.Equal(t, k8sView.Table.GetTitle(), "[ K8S-Object ]")
assert.Equal(t, k8sView.Table.GetTitle(), "[ K8S-Object (all/all) ]")
assert.Equal(t, k8sView.GetCell(0, 0).Text, "Name")
})
@@ -77,9 +77,15 @@ func TestK8SView(t *testing.T) {
})
t.Run("hint", func(t *testing.T) {
t.Run("hint", func(t *testing.T) {
assert.Equal(t, len(k8sView.Hint()), 2)
})
assert.Equal(t, len(k8sView.Hint()), 4)
})
t.Run("select cluster", func(t *testing.T) {
assert.Empty(t, k8sView.clusterView(nil))
})
t.Run("select cluster namespace", func(t *testing.T) {
assert.Empty(t, k8sView.clusterNamespaceView(nil))
})
}

View File

@@ -0,0 +1,94 @@
/*
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"
)
// NamespaceView is namespace view struct
type NamespaceView struct {
*ResourceView
ctx context.Context
}
// NewNamespaceView return a new namespace view
func NewNamespaceView(ctx context.Context, app *App) model.Component {
v := &NamespaceView{
ResourceView: NewResourceView(app),
ctx: ctx,
}
return v
}
// Init a namespace view
func (v *NamespaceView) Init() {
title := fmt.Sprintf("[ %s ]", v.Name())
v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor)
resourceList := v.ListNamespaces()
v.ResourceView.Init(resourceList)
v.bindKeys()
}
// ListNamespaces return all namespaces
func (v *NamespaceView) ListNamespaces() model.ResourceList {
return model.ListNamespaces(v.ctx, v.app.client)
}
// Hint return key action menu hints of the k8s view
func (v *NamespaceView) Hint() []model.MenuHint {
return v.Actions().Hint()
}
// Name return k8s view name
func (v *NamespaceView) Name() string {
return "Namespace"
}
func (v *NamespaceView) bindKeys() {
v.Actions().Delete([]tcell.Key{tcell.KeyEnter})
v.Actions().Add(model.KeyActions{
tcell.KeyEnter: model.KeyAction{Description: "Select", Action: v.applicationView, 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},
})
}
func (v *NamespaceView) applicationView(event *tcell.EventKey) *tcell.EventKey {
row, _ := v.GetSelection()
if row == 0 {
return event
}
v.app.content.PopComponent()
ns := v.Table.GetCell(row, 0).Text
if ns == model.AllNamespace {
ns = ""
}
v.ctx = context.WithValue(v.ctx, &model.CtxKeyNamespace, ns)
v.app.command.run(v.ctx, "app")
return event
}

View File

@@ -38,7 +38,7 @@ func TestPageStack(t *testing.T) {
assert.NoError(t, err)
testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme})
assert.NoError(t, err)
app := NewApp(testClient, cfg)
app := NewApp(testClient, cfg, "")
assert.Equal(t, len(app.Components), 4)
stack := NewPageStack(app)

View File

@@ -47,6 +47,12 @@ var ResourceMap = map[string]ResourceViewer{
"k8s": {
viewFunc: NewK8SView,
},
"ns": {
viewFunc: NewNamespaceView,
},
"cns": {
viewFunc: NewClusterNamespaceView,
},
}
// NewResourceView return a new resource view