diff --git a/apistructs/component_protocol.go b/apistructs/component_protocol.go index 8b05474a86a..e3f47d31e54 100644 --- a/apistructs/component_protocol.go +++ b/apistructs/component_protocol.go @@ -121,6 +121,7 @@ const ( OnCancel OperationKey = "cancel" OnChangePageNoOperation OperationKey = "changePageNo" OnChangePageSizeOperation OperationKey = "changePageSize" + OnChangeSortOperation OperationKey = "changeSort" // Issue MoveOutOperation OperationKey = "MoveOut" DragOperation OperationKey = "drag" diff --git a/apistructs/steve.go b/apistructs/steve.go index a0a035adb8f..a705a64068b 100644 --- a/apistructs/steve.go +++ b/apistructs/steve.go @@ -18,8 +18,6 @@ import ( "encoding/json" "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/erda-project/erda/pkg/strutil" ) @@ -27,20 +25,6 @@ const ( SteveErrorType = "error" ) -// SteveCollection is a resource collection returned from steve server. -type SteveCollection struct { - Type string `json:"type,omitempty"` - Links map[string]string `json:"links"` - CreateTypes map[string]string `json:"createTypes,omitempty"` - Actions map[string]string `json:"actions"` - ResourceType string `json:"resourceType"` - Revision string `json:"revision"` - Pagination *Pagination `json:"pagination,omitempty"` - Continue string `json:"continue,omitempty"` - // steve resources - Data []SteveResource `json:"data"` -} - var ( BadRequest = SteveErrorCode{"BadRequest", 400} Unauthorized = SteveErrorCode{"Unauthorized", 401} @@ -81,31 +65,6 @@ func (s SteveError) JSON() []byte { return data } -// SteveResource is a steve resource returned from steve server. -type SteveResource struct { - K8SResource - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Links map[string]string `json:"links"` -} - -// K8SResource is a original k8s resource. -type K8SResource struct { - metav1.TypeMeta - Metadata metav1.ObjectMeta `json:"metadata,omitempty"` - Spec interface{} `json:"spec,omitempty"` - Status interface{} `json:"status,omitempty"` - Extra interface{} `json:"extra,omitempty"` -} - -// Pagination used to paging query. -type Pagination struct { - Limit int `json:"limit,omitempty"` // maximum number of each page - First string `json:"first,omitempty"` // first page link - Next string `json:"next,omitempty"` // next page link - Partial bool `json:"partial,omitempty"` // whether partial -} - type K8SResType string const ( @@ -115,6 +74,9 @@ const ( K8SReplicaSet K8SResType = "apps.replicasets" K8SDaemonSet K8SResType = "apps.daemonsets" K8SStatefulSet K8SResType = "apps.statefulsets" + K8SJob K8SResType = "batch.jobs" + K8SCronJob K8SResType = "batch.cronjobs" + K8SNamespace K8SResType = "namespace" K8SEvent K8SResType = "events" ) @@ -153,16 +115,3 @@ func (k *SteveRequest) URLQueryString() map[string][]string { } return query } - -func GetValue(obj interface{}, keys ...string) (interface{}, bool) { - data, _ := obj.(map[string]interface{}) - for i, key := range keys { - if i == len(keys)-1 { - val, ok := data[key] - return val, ok - } - data, _ = data[key].(map[string]interface{}) - } - - return nil, false -} diff --git a/bundle/steve.go b/bundle/steve.go index 8d1c3878818..837827dee6a 100644 --- a/bundle/steve.go +++ b/bundle/steve.go @@ -19,8 +19,10 @@ import ( "io/ioutil" "net/http" "reflect" + "strconv" "github.com/pkg/errors" + "github.com/rancher/wrangler/pkg/data" "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/bundle/apierrors" @@ -32,7 +34,7 @@ import ( // GetSteveResource gets k8s resource from steve server. // Required fields: ClusterName, Name, Type. -func (b *Bundle) GetSteveResource(req *apistructs.SteveRequest) (*apistructs.SteveResource, error) { +func (b *Bundle) GetSteveResource(req *apistructs.SteveRequest) (data.Object, error) { if req.Type == "" || req.ClusterName == "" || req.Name == "" { return nil, errors.New("clusterName, name and type fields are required") } @@ -61,20 +63,20 @@ func (b *Bundle) GetSteveResource(req *apistructs.SteveRequest) (*apistructs.Ste return nil, apierrors.ErrInvoke.InternalError(err) } - if err = isSteveError(data); err != nil { - return nil, err + obj := map[string]interface{}{} + if err = json.Unmarshal(data, &obj); err != nil { + return nil, apierrors.ErrInvoke.InternalError(err) } - var resource apistructs.SteveResource - if err = json.Unmarshal(data, &resource); err != nil { - return nil, apierrors.ErrInvoke.InternalError(err) + if err = isSteveError(obj); err != nil { + return nil, err } - return &resource, nil + return obj, nil } // ListSteveResource lists k8s resource from steve server. // Required fields: ClusterName, Type. -func (b *Bundle) ListSteveResource(req *apistructs.SteveRequest) (*apistructs.SteveCollection, error) { +func (b *Bundle) ListSteveResource(req *apistructs.SteveRequest) (data.Object, error) { if req.Type == "" || req.ClusterName == "" { return nil, errors.New("clusterName and type fields are required") } @@ -103,20 +105,20 @@ func (b *Bundle) ListSteveResource(req *apistructs.SteveRequest) (*apistructs.St return nil, apierrors.ErrInvoke.InternalError(err) } - if err = isSteveError(data); err != nil { - return nil, err + obj := map[string]interface{}{} + if err = json.Unmarshal(data, &obj); err != nil { + return nil, apierrors.ErrInvoke.InternalError(err) } - var collection apistructs.SteveCollection - if err = json.Unmarshal(data, &collection); err != nil { - return nil, apierrors.ErrInvoke.InternalError(err) + if err = isSteveError(obj); err != nil { + return nil, err } - return &collection, nil + return obj, nil } // UpdateSteveResource update a k8s resource described by req.Obj from steve server. // Required fields: ClusterName, Type, Name, Obj -func (b *Bundle) UpdateSteveResource(req *apistructs.SteveRequest) (*apistructs.SteveResource, error) { +func (b *Bundle) UpdateSteveResource(req *apistructs.SteveRequest) (data.Object, error) { if req.Type == "" || req.ClusterName == "" || req.Name == "" { return nil, errors.New("clusterName, name and type fields are required") } @@ -147,20 +149,20 @@ func (b *Bundle) UpdateSteveResource(req *apistructs.SteveRequest) (*apistructs. return nil, apierrors.ErrInvoke.InternalError(err) } - if err = isSteveError(data); err != nil { - return nil, err + obj := map[string]interface{}{} + if err = json.Unmarshal(data, &obj); err != nil { + return nil, apierrors.ErrInvoke.InternalError(err) } - var resource apistructs.SteveResource - if err = json.Unmarshal(data, &resource); err != nil { - return nil, apierrors.ErrInvoke.InternalError(err) + if err = isSteveError(obj); err != nil { + return nil, err } - return &resource, nil + return obj, nil } // CreateSteveResource creates a k8s resource described by req.Obj from steve server. // Required fields: ClusterName, Type, Obj -func (b *Bundle) CreateSteveResource(req *apistructs.SteveRequest) (*apistructs.SteveResource, error) { +func (b *Bundle) CreateSteveResource(req *apistructs.SteveRequest) (data.Object, error) { if req.Type == "" || req.ClusterName == "" { return nil, errors.New("clusterName and type fields are required") } @@ -191,15 +193,15 @@ func (b *Bundle) CreateSteveResource(req *apistructs.SteveRequest) (*apistructs. return nil, apierrors.ErrInvoke.InternalError(err) } - if err = isSteveError(data); err != nil { - return nil, err + obj := map[string]interface{}{} + if err = json.Unmarshal(data, &obj); err != nil { + return nil, apierrors.ErrInvoke.InternalError(err) } - var resource apistructs.SteveResource - if err = json.Unmarshal(data, &resource); err != nil { - return nil, apierrors.ErrInvoke.InternalError(err) + if err = isSteveError(obj); err != nil { + return nil, err } - return &resource, nil + return obj, nil } // DeleteSteveResource delete a k8s resource from steve server. @@ -232,7 +234,11 @@ func (b *Bundle) DeleteSteveResource(req *apistructs.SteveRequest) error { return apierrors.ErrInvoke.InternalError(err) } - return isSteveError(data) + obj := map[string]interface{}{} + if err = json.Unmarshal(data, &obj); err != nil { + return apierrors.ErrInvoke.InternalError(err) + } + return isSteveError(obj) } func isObjInvalid(obj interface{}) bool { @@ -240,31 +246,15 @@ func isObjInvalid(obj interface{}) bool { return v.Kind() == reflect.Ptr && !v.IsNil() } -func isSteveError(data []byte) error { - if len(data) == 0 { - return nil - } - var obj map[string]interface{} - err := json.Unmarshal(data, &obj) - if err != nil { - return apierrors.ErrInvoke.InternalError(err) - } - - typ, ok := obj["type"].(string) - if !ok { - return apierrors.ErrInvoke.InternalError(errors.New("type field is null")) - } - - if typ != apistructs.SteveErrorType { +func isSteveError(obj data.Object) error { + if obj.String("type") != "error" { return nil } - - var steveErr apistructs.SteveError - if err = json.Unmarshal(data, &steveErr); err != nil { - return apierrors.ErrInvoke.InternalError(err) - } - return toAPIError(steveErr.Status, apistructs.ErrorResponse{ - Code: steveErr.Code, - Msg: steveErr.Message, + status, _ := strconv.ParseInt(obj.String("status"), 10, 64) + code := obj.String("code") + message := obj.String("message") + return toAPIError(int(status), apistructs.ErrorResponse{ + Code: code, + Msg: message, }) } diff --git a/bundle/steve_node.go b/bundle/steve_node.go index 8683838fe64..f7d22cc4a55 100644 --- a/bundle/steve_node.go +++ b/bundle/steve_node.go @@ -15,6 +15,7 @@ package bundle import ( + "encoding/json" "errors" "io/ioutil" "net/http" @@ -57,7 +58,12 @@ func (b *Bundle) PatchNode(req *apistructs.SteveRequest) error { if err != nil { return apierrors.ErrInvoke.InternalError(err) } - return isSteveError(data) + + obj := map[string]interface{}{} + if err = json.Unmarshal(data, &obj); err != nil { + return apierrors.ErrInvoke.InternalError(err) + } + return isSteveError(obj) } // LabelNode labels a node. diff --git a/modules/cmp/steve/aggregator.go b/modules/cmp/steve/aggregator.go index 28810a3babb..2eb4d3b391b 100644 --- a/modules/cmp/steve/aggregator.go +++ b/modules/cmp/steve/aggregator.go @@ -226,11 +226,6 @@ func (a *Aggregator) ServeHTTP(rw http.ResponseWriter, req *http.Request) { a.Add(cluster) if s, ok = a.servers.Load(cluster.Name); !ok { rw.WriteHeader(http.StatusInternalServerError) - rw.Write(apistructs.SteveError{ - SteveErrorCode: apistructs.ServerError, - Type: "error", - Message: "Internal server error", - }.JSON()) rw.Write(apistructs.NewSteveError(apistructs.ServerError, "Internal server error").JSON()) } } diff --git a/modules/openapi/api/apis/cmp/cmp_k8s_create.go b/modules/openapi/api/apis/cmp/cmp_k8s_create.go index cbc1d91c614..c0cb9c767e1 100644 --- a/modules/openapi/api/apis/cmp/cmp_k8s_create.go +++ b/modules/openapi/api/apis/cmp/cmp_k8s_create.go @@ -15,7 +15,8 @@ package cmp import ( - "github.com/erda-project/erda/apistructs" + "github.com/rancher/apiserver/pkg/types" + "github.com/erda-project/erda/modules/openapi/api/apis" ) @@ -29,7 +30,6 @@ var CMP_STEVE_CREATE = apis.ApiSpec{ Audit: nil, CheckLogin: true, Doc: "create a k8s resource", - RequestType: apistructs.K8SResource{}, - ResponseType: apistructs.SteveCollection{}, + ResponseType: types.RawResource{}, IsOpenAPI: true, } diff --git a/modules/openapi/api/apis/cmp/cmp_k8s_get.go b/modules/openapi/api/apis/cmp/cmp_k8s_get.go index 14503c79306..c716d04917a 100644 --- a/modules/openapi/api/apis/cmp/cmp_k8s_get.go +++ b/modules/openapi/api/apis/cmp/cmp_k8s_get.go @@ -15,7 +15,8 @@ package cmp import ( - "github.com/erda-project/erda/apistructs" + "github.com/rancher/apiserver/pkg/types" + "github.com/erda-project/erda/modules/openapi/api/apis" ) @@ -29,6 +30,6 @@ var CMP_STEVE_GET = apis.ApiSpec{ Audit: nil, CheckLogin: true, Doc: "get a k8s resource", - ResponseType: apistructs.SteveResource{}, + ResponseType: types.RawResource{}, IsOpenAPI: true, } diff --git a/modules/openapi/api/apis/cmp/cmp_k8s_list.go b/modules/openapi/api/apis/cmp/cmp_k8s_list.go index 027c8e70e23..05be0ad729e 100644 --- a/modules/openapi/api/apis/cmp/cmp_k8s_list.go +++ b/modules/openapi/api/apis/cmp/cmp_k8s_list.go @@ -15,7 +15,8 @@ package cmp import ( - "github.com/erda-project/erda/apistructs" + "github.com/rancher/apiserver/pkg/types" + "github.com/erda-project/erda/modules/openapi/api/apis" ) @@ -29,6 +30,6 @@ var CMP_STEVE_LIST = apis.ApiSpec{ Audit: nil, CheckLogin: true, Doc: "list a type of k8s resource", - ResponseType: apistructs.SteveCollection{}, + ResponseType: types.GenericCollection{}, IsOpenAPI: true, } diff --git a/modules/openapi/api/apis/cmp/cmp_k8s_update.go b/modules/openapi/api/apis/cmp/cmp_k8s_update.go index ff0e0ff9a21..8ebe0da09bb 100644 --- a/modules/openapi/api/apis/cmp/cmp_k8s_update.go +++ b/modules/openapi/api/apis/cmp/cmp_k8s_update.go @@ -15,7 +15,8 @@ package cmp import ( - "github.com/erda-project/erda/apistructs" + "github.com/rancher/apiserver/pkg/types" + "github.com/erda-project/erda/modules/openapi/api/apis" ) @@ -29,7 +30,6 @@ var CMP_STEVE_UPDATE = apis.ApiSpec{ Audit: nil, CheckLogin: true, Doc: "update a k8s resource", - RequestType: apistructs.K8SResource{}, - ResponseType: apistructs.SteveResource{}, + ResponseType: types.RawResource{}, IsOpenAPI: true, } diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/model.go b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/model.go new file mode 100644 index 00000000000..d8a8ef73139 --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/model.go @@ -0,0 +1,81 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 eventTable + +import protocol "github.com/erda-project/erda/modules/openapi/component-protocol" + +type ComponentEventTable struct { + ctxBdl protocol.ContextBundle + + Type string `json:"type,omitempty"` + State State `json:"state,omitempty"` + Props Props `json:"props,omitempty"` + Data Data `json:"data,omitempty"` + Operations map[string]interface{} `json:"operations,omitempty"` +} + +type State struct { + PageNo uint64 `json:"pageNo,omitempty"` + PageSize uint64 `json:"pageSize,omitempty"` + Total uint64 `json:"total"` + Sorter Sorter `json:"sorterData,omitempty"` + ClusterName string `json:"clusterName,omitempty"` + FilterValues FilterValues `json:"filterValues,omitempty"` +} + +type FilterValues struct { + Namespace []string `json:"namespace,omitempty"` + Type []string `json:"type,omitempty"` +} + +type Data struct { + List []Item `json:"list"` +} + +type Item struct { + LastSeen string `json:"lastSeen"` + LastSeenTimestamp int64 `json:"lastSeenTimestamp"` + Type string `json:"type"` + Reason string `json:"reason"` + Object string `json:"object"` + Source string `json:"source"` + Message string `json:"message"` + Count string `json:"count"` + CountNum int64 `json:"countNum"` + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +type Props struct { + PageSizeOptions []string `json:"pageSizeOptions"` + Columns []Column `json:"columns"` +} + +type Column struct { + DataIndex string `json:"dataIndex"` + Title string `json:"title"` + Width string `json:"width"` + Sorter bool `json:"sorter,omitempty"` +} + +type Operation struct { + Key string `json:"key,omitempty"` + Reload bool `json:"reload"` +} + +type Sorter struct { + Field string `json:"field,omitempty"` + Order string `json:"order,omitempty"` +} diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/render.go b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/render.go new file mode 100644 index 00000000000..076d3c59dff --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/render.go @@ -0,0 +1,322 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 eventTable + +import ( + "context" + "encoding/json" + "strconv" + "time" + + "github.com/pkg/errors" + "github.com/recallsong/go-utils/container/slice" + "github.com/sirupsen/logrus" + + "github.com/erda-project/erda/apistructs" + protocol "github.com/erda-project/erda/modules/openapi/component-protocol" +) + +func RenderCreator() protocol.CompRender { + return &ComponentEventTable{} +} + +func (t *ComponentEventTable) Render(ctx context.Context, component *apistructs.Component, _ apistructs.ComponentProtocolScenario, + event apistructs.ComponentEvent, globalStateData *apistructs.GlobalStateData) error { + bdl := ctx.Value(protocol.GlobalInnerKeyCtxBundle.String()).(protocol.ContextBundle) + if bdl.Bdl == nil { + return errors.New("context bundle can not be empty") + } + t.ctxBdl = bdl + if err := t.GenComponentState(component); err != nil { + return err + } + + // set page no. and page size in first render + if event.Operation == apistructs.InitializeOperation { + t.State.PageNo = 1 + t.State.PageSize = 20 + } + // set page no. if triggered by filter + if event.Operation == apistructs.RenderingOperation || event.Operation == apistructs.OnChangeSortOperation || + event.Operation == apistructs.ChangeOrgsPageSizeOperationKey { + t.State.PageNo = 1 + } + if err := t.RenderList(); err != nil { + return err + } + t.SetComponentValue() + return nil +} + +func (t *ComponentEventTable) GenComponentState(component *apistructs.Component) error { + if component == nil || component.State == nil { + return nil + } + + data, err := json.Marshal(component.State) + if err != nil { + logrus.Errorf("failed to marshal for eventTable state, %v", err) + return err + } + var state State + err = json.Unmarshal(data, &state) + if err != nil { + logrus.Errorf("failed to unmarshal for eventTable state, %v", err) + return err + } + t.State = state + return nil +} + +func (t *ComponentEventTable) RenderList() error { + userID := t.ctxBdl.Identity.UserID + orgID := t.ctxBdl.Identity.OrgID + + req := apistructs.SteveRequest{ + UserID: userID, + OrgID: orgID, + Type: apistructs.K8SEvent, + ClusterName: t.State.ClusterName, + } + + obj, err := t.ctxBdl.Bdl.ListSteveResource(&req) + if err != nil { + return err + } + list := obj.Slice("data") + + var items []Item + for _, obj := range list { + if t.State.FilterValues.Namespace != nil && !contain(t.State.FilterValues.Namespace, obj.String("metadata", "namespace")) { + continue + } + if t.State.FilterValues.Type != nil && !contain(t.State.FilterValues.Type, obj.String("_type")) { + continue + } + fields := obj.StringSlice("metadata", "fields") + if len(fields) != 10 { + logrus.Errorf("length of event fields is invalid: %d", len(fields)) + continue + } + count, err := strconv.ParseInt(fields[8], 10, 64) + if err != nil { + logrus.Errorf("failed to parse count for event %s, %v", fields[9], err) + continue + } + lastSeenTimestamp, err := time.ParseDuration(fields[0]) + if err != nil { + logrus.Errorf("failed to parse timestamp for event %s, %v", fields[9], err) + continue + } + items = append(items, Item{ + LastSeen: fields[0], + LastSeenTimestamp: lastSeenTimestamp.Nanoseconds(), + Type: fields[1], + Reason: fields[2], + Object: fields[3], + Source: fields[5], + Message: fields[6], + Count: fields[8], + CountNum: count, + Name: fields[9], + Namespace: obj.String("metadata", "namespace"), + }) + } + if t.State.Sorter.Field != "" { + cmpWrapper := func(field, order string) func(int, int) bool { + ascend := order == "ascend" + switch field { + case "lastSeen": + return func(i int, j int) bool { + less := items[i].LastSeenTimestamp < items[j].LastSeenTimestamp + if ascend { + return less + } + return !less + } + case "type": + return func(i int, j int) bool { + less := items[i].Type < items[j].Type + if ascend { + return less + } + return !less + } + case "reason": + return func(i int, j int) bool { + less := items[i].Reason < items[j].Reason + if ascend { + return less + } + return !less + } + case "object": + return func(i int, j int) bool { + less := items[i].Object < items[j].Object + if ascend { + return less + } + return !less + } + case "source": + return func(i int, j int) bool { + less := items[i].Source < items[j].Source + if ascend { + return less + } + return !less + } + case "message": + return func(i int, j int) bool { + less := items[i].Message < items[j].Message + if ascend { + return less + } + return !less + } + case "count": + return func(i int, j int) bool { + less := items[i].CountNum < items[j].CountNum + if ascend { + return less + } + return !less + } + case "name": + return func(i int, j int) bool { + less := items[i].Name < items[j].Name + if ascend { + return less + } + return !less + } + case "namespace": + return func(i int, j int) bool { + less := items[i].Namespace < items[j].Namespace + if ascend { + return less + } + return !less + } + default: + return func(i int, j int) bool { + return false + } + } + } + slice.Sort(items, cmpWrapper(t.State.Sorter.Field, t.State.Sorter.Order)) + } + + l, r := getRange(len(items), int(t.State.PageNo), int(t.State.PageSize)) + t.Data.List = items[l:r] + t.State.Total = uint64(len(items)) + return nil +} + +func (t *ComponentEventTable) SetComponentValue() { + t.Props = Props{ + PageSizeOptions: []string{"10", "20", "50", "100"}, + Columns: []Column{ + { + DataIndex: "lastSeen", + Title: "Last Seen", + Width: "160", + Sorter: true, + }, + { + DataIndex: "type", + Title: "Event Type", + Width: "100", + Sorter: true, + }, + { + DataIndex: "reason", + Title: "Reason", + Width: "100", + Sorter: true, + }, + { + DataIndex: "object", + Title: "Object", + Width: "150", + Sorter: true, + }, + { + DataIndex: "source", + Title: "Source", + Width: "120", + Sorter: true, + }, + { + DataIndex: "message", + Title: "Message", + Width: "120", + Sorter: true, + }, + { + DataIndex: "count", + Title: "Count", + Width: "80", + Sorter: true, + }, + { + DataIndex: "name", + Title: "Name", + Width: "120", + Sorter: true, + }, + { + DataIndex: "namespace", + Title: "Namespace", + Width: "120", + Sorter: true, + }, + }, + } + t.Operations = make(map[string]interface{}) + t.Operations[apistructs.OnChangePageNoOperation.String()] = Operation{ + Key: apistructs.OnChangePageNoOperation.String(), + Reload: true, + } + t.Operations[apistructs.OnChangePageSizeOperation.String()] = Operation{ + Key: apistructs.OnChangePageSizeOperation.String(), + Reload: true, + } + t.Operations[apistructs.OnChangeSortOperation.String()] = Operation{ + Key: apistructs.OnChangeSortOperation.String(), + Reload: true, + } +} + +func contain(arr []string, target string) bool { + for _, str := range arr { + if target == str { + return true + } + } + return false +} + +func getRange(length, pageNo, pageSize int) (int, int) { + l := (pageNo - 1) * pageSize + if l >= length { + l = 0 + } + r := l + pageSize + if r > length { + r = length + } + return l, r +} diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/render_test.go b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/render_test.go new file mode 100644 index 00000000000..02f70d65053 --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/eventTable/render_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 eventTable + +import ( + "testing" + + "github.com/erda-project/erda/apistructs" +) + +func TestContain(t *testing.T) { + arr := []string{ + "a", "b", "c", "d", + } + if contain(arr, "e") { + t.Errorf("test failed, expected not contain \"e\", actual do") + } + if !contain(arr, "a") || !contain(arr, "b") || !contain(arr, "c") || !contain(arr, "d") { + t.Errorf("test failed, expected contain \"a\" , \"b\", \"c\" and \"d\", actual not") + } +} + +func TestGetRange(t *testing.T) { + length := 0 + pageNo := 1 + pageSize := 20 + l, r := getRange(length, pageNo, pageSize) + if l != 0 { + t.Errorf("test failed, l is unexpected, expected 0, actual %d", l) + } + if r != 0 { + t.Errorf("test failed, r is unexpected, expected 0, actual %d", r) + } + + length = 21 + pageNo = 2 + pageSize = 20 + l, r = getRange(length, pageNo, pageSize) + if l != 20 { + t.Errorf("test failed, l is unexpected, expected 20, actual %d", l) + } + if r != 21 { + t.Errorf("test failed, r is unexpected, expected 21, actual %d", r) + } + + length = 20 + pageNo = 2 + pageSize = 50 + l, r = getRange(length, pageNo, pageSize) + if l != 0 { + t.Errorf("test failed, l is unexpected, expected 0, actual %d", l) + } + if r != 20 { + t.Errorf("test failed, r is unexpected, expected 20, actual %d", r) + } +} + +func TestComponentEventTable_SetComponentValue(t *testing.T) { + cet := &ComponentEventTable{} + cet.SetComponentValue() + if len(cet.Props.PageSizeOptions) != 4 { + t.Errorf("test failed, len of .Props.PageSizeOptions is unexpected, expected 4, actual %d", len(cet.Props.PageSizeOptions)) + } + if len(cet.Props.Columns) != 9 { + t.Errorf("test failed, len of .Props.Columns is unexpected, expected 9, actual %d", len(cet.Props.Columns)) + } + if cet.Operations == nil { + t.Errorf("test failed, .Operations is unexpected, expected not null, actual null") + } + if _, ok := cet.Operations[apistructs.OnChangeSortOperation.String()]; !ok { + t.Errorf("test failed, .Operations is unexpected, %s is not existed", apistructs.OnChangeSortOperation.String()) + } + if _, ok := cet.Operations[apistructs.OnChangePageNoOperation.String()]; !ok { + t.Errorf("test failed, .Operations is unexpected, %s is not existed", apistructs.OnChangePageNoOperation.String()) + } + if _, ok := cet.Operations[apistructs.OnChangePageSizeOperation.String()]; !ok { + t.Errorf("test failed, .Operations is unexpected, %s is not existed", apistructs.OnChangePageSizeOperation.String()) + } +} \ No newline at end of file diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/model.go b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/model.go new file mode 100644 index 00000000000..d9d8c3d93e7 --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/model.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 filter + +import protocol "github.com/erda-project/erda/modules/openapi/component-protocol" + +type ComponentFilter struct { + ctxBdl protocol.ContextBundle + + Type string `json:"type,omitempty"` + State State `json:"state,omitempty"` + Operations map[string]interface{} `json:"operations,omitempty"` +} + +type State struct { + ClusterName string `json:"clusterName,omitempty"` + Conditions []Condition `json:"conditions,omitempty"` + Values Values `json:"values,omitempty"` +} + +type Values struct { + Type []string `json:"type,omitempty"` + Namespace []string `json:"namespace,omitempty"` +} + +type Condition struct { + Key string `json:"key,omitempty"` + Label string `json:"label,omitempty"` + Type string `json:"type,omitempty"` + Fixed bool `json:"fixed"` + Options []Option `json:"options,omitempty"` +} + +type Option struct { + Label string `json:"label,omitempty"` + Value string `json:"value,omitempty"` + Children []Option `json:"children,omitempty"` +} + +type Operation struct { + Key string `json:"key,omitempty"` + Reload bool `json:"reload"` +} diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/render.go b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/render.go new file mode 100644 index 00000000000..c8e585e7614 --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/render.go @@ -0,0 +1,235 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 filter + +import ( + "context" + "encoding/json" + "sort" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/erda-project/erda/apistructs" + protocol "github.com/erda-project/erda/modules/openapi/component-protocol" +) + +func RenderCreator() protocol.CompRender { + return &ComponentFilter{} +} + +func (f *ComponentFilter) Render(ctx context.Context, component *apistructs.Component, _ apistructs.ComponentProtocolScenario, + _ apistructs.ComponentEvent, _ *apistructs.GlobalStateData) error { + bdl := ctx.Value(protocol.GlobalInnerKeyCtxBundle.String()).(protocol.ContextBundle) + if bdl.Bdl == nil { + return errors.New("context bundle can not be empty") + } + f.ctxBdl = bdl + + if err := f.GenComponentState(component); err != nil { + return err + } + + if err := f.SetComponentValue(); err != nil { + return err + } + return nil +} + +func (f *ComponentFilter) GenComponentState(c *apistructs.Component) error { + if c == nil || c.State == nil { + return nil + } + var state State + cont, err := json.Marshal(c.State) + if err != nil { + logrus.Errorf("marshal component state failed, content:%v, err:%v", c.State, err) + return err + } + err = json.Unmarshal(cont, &state) + if err != nil { + logrus.Errorf("unmarshal component state failed, content:%v, err:%v", cont, err) + return err + } + f.State = state + return nil +} + +func (f *ComponentFilter) SetComponentValue() error { + userID := f.ctxBdl.Identity.UserID + orgID := f.ctxBdl.Identity.OrgID + + req := apistructs.SteveRequest{ + UserID: userID, + OrgID: orgID, + Type: apistructs.K8SNamespace, + ClusterName: f.State.ClusterName, + } + + data, err := f.ctxBdl.Bdl.ListSteveResource(&req) + if err != nil { + return err + } + list := data.Slice("data") + + devNs := Option{ + Label: "dev", + Value: "dev", + } + testNs := Option{ + Label: "test", + Value: "test", + } + stagingNs := Option{ + Label: "staging", + Value: "staging", + } + productionNs := Option{ + Label: "production", + Value: "production", + } + addonNs := Option{ + Label: "addons", + Value: "addons", + } + pipelineNs := Option{ + Label: "pipelines", + Value: "pipelines", + } + defaultNs := Option{ + Label: "default", + Value: "default", + } + systemNs := Option{ + Label: "system", + Value: "system", + } + otherNs := Option{ + Label: "others", + Value: "others", + } + + for _, obj := range list { + name := obj.String("metadata", "name") + option := Option{ + Label: name, + Value: name, + } + if suf, ok := hasSuffix(name); ok && strings.HasPrefix(name, "project-") { + displayName, err := f.getDisplayName(name) + if err == nil { + option.Label = displayName + switch suf { + case "-dev": + devNs.Children = append(devNs.Children, option) + case "-test": + testNs.Children = append(testNs.Children, option) + case "-prod": + productionNs.Children = append(productionNs.Children, option) + case "-staging": + stagingNs.Children = append(stagingNs.Children, option) + } + continue + } + } + if strings.HasPrefix(name, "addon-") || strings.HasPrefix(name, "group-addon-") { + addonNs.Children = append(addonNs.Children, option) + continue + } + if strings.HasPrefix(name, "pipeline-") { + pipelineNs.Children = append(pipelineNs.Children, option) + continue + } + if name == "default" { + defaultNs.Children = append(defaultNs.Children, option) + continue + } + if name == "kube-system" || name == "erda-system" { + systemNs.Children = append(systemNs.Children, option) + continue + } + otherNs.Children = append(otherNs.Children, option) + } + + f.State.Conditions = nil + namespaceCond := Condition{ + Key: "namespace", + Label: "Namespace", + Type: "select", + Fixed: true, + } + for _, option := range []Option{devNs, testNs, productionNs, stagingNs, addonNs, pipelineNs, defaultNs, systemNs, otherNs} { + if option.Children != nil { + sort.Slice(option.Children, func(i, j int) bool { + return option.Children[i].Label < option.Children[j].Label + }) + namespaceCond.Options = append(namespaceCond.Options, option) + } + } + f.State.Conditions = append(f.State.Conditions, namespaceCond) + + f.State.Conditions = append(f.State.Conditions, Condition{ + Key: "type", + Label: "Event Type", + Type: "select", + Fixed: true, + Options: []Option{ + { + Label: "Normal", + Value: "Normal", + }, + { + Label: "Warning", + Value: "Warning", + }, + }, + }) + + f.Operations = make(map[string]interface{}) + f.Operations["filter"] = Operation{ + Key: "filter", + Reload: true, + } + return nil +} + +func (f *ComponentFilter) getDisplayName(name string) (string, error) { + splits := strings.Split(name, "-") + if len(splits) != 3 { + return "", errors.New("invalid name") + } + id := splits[1] + num, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return "", err + } + project, err := f.ctxBdl.Bdl.GetProject(uint64(num)) + if err != nil { + return "", err + } + return project.DisplayName, nil +} + +func hasSuffix(name string) (string, bool) { + suffixes := []string{"-dev", "-staging", "-test", "-prod"} + for _, suffix := range suffixes { + if strings.HasSuffix(name, suffix) { + return suffix, true + } + } + return "", false +} diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/render_test.go b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/render_test.go new file mode 100644 index 00000000000..805db905ee8 --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/components/filter/render_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 filter + +import "testing" + +func TestHasSuffix(t *testing.T) { + if _, ok := hasSuffix("project-1-dev"); !ok { + t.Errorf("test failed, \"project-1-dev\" has project suffix, actual not") + } + if _, ok := hasSuffix("project-2-staging"); !ok { + t.Errorf("test failed, \"project-2-staging\" has project suffix, actual not") + } + if _, ok := hasSuffix("project-3-test"); !ok { + t.Errorf("test failed, \"project-3-test\" has project suffix, actual not") + } + if _, ok := hasSuffix("project-4-prod"); !ok { + t.Errorf("test failed, \"project-4-prod\" has project suffix, actual not") + } + if _, ok := hasSuffix("project-5-custom"); ok { + t.Errorf("test failed, \"project-5-custom\" does not have project suffix, actul do") + } +} \ No newline at end of file diff --git a/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/protocol.yml b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/protocol.yml new file mode 100644 index 00000000000..612449d0ea7 --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/cmp-dashboard-events/protocol.yml @@ -0,0 +1,35 @@ +version: 0.1 + +scenario: "cmp-dashboard-events" + +hierarchy: + root: page + structure: + page: + - filter + - eventTable + +components: + page: + type: Container + filter: + type: ContractiveFilter + eventTable: + type: Table + +rendering: + __DefaultRendering__: + - name: page + - name: filter + state: + - name: "clusterName" + value: "{{ __InParams__.clusterName }}" + - name: eventTable + state: + - name: "clusterName" + value: "{{ __InParams__.clusterName }}" + filter: + - name: eventTable + state: + - name: "filterValues" + value: "{{ filter.values }}" \ No newline at end of file