diff --git a/assets/swagger.json b/assets/swagger.json index c155555315d97..1ecc59993e088 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -2011,6 +2011,42 @@ } } }, + "/api/v1/applicationsets/{name}/events": { + "get": { + "tags": [ + "ApplicationSetService" + ], + "summary": "ListResourceEvents returns a list of event resources", + "operationId": "ApplicationSetService_ListResourceEvents", + "parameters": [ + { + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "appsetNamespace", + "in": "query" + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1EventList" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/runtimeError" + } + } + } + } + }, "/api/v1/certificates": { "get": { "tags": [ diff --git a/pkg/apiclient/applicationset/applicationset.pb.go b/pkg/apiclient/applicationset/applicationset.pb.go index 8f717d1f6920f..818e4b4411ab4 100644 --- a/pkg/apiclient/applicationset/applicationset.pb.go +++ b/pkg/apiclient/applicationset/applicationset.pb.go @@ -17,6 +17,7 @@ import ( codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" io "io" + v1 "k8s.io/api/core/v1" math "math" math_bits "math/bits" ) @@ -156,6 +157,62 @@ func (m *ApplicationSetListQuery) GetAppsetNamespace() string { return "" } +// ApplicationSetEventsQuery is a query for applicationset resource events +type ApplicationSetResourceEventsQuery struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + AppsetNamespace string `protobuf:"bytes,5,opt,name=appsetNamespace,proto3" json:"appsetNamespace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ApplicationSetResourceEventsQuery) Reset() { *m = ApplicationSetResourceEventsQuery{} } +func (m *ApplicationSetResourceEventsQuery) String() string { return proto.CompactTextString(m) } +func (*ApplicationSetResourceEventsQuery) ProtoMessage() {} +func (*ApplicationSetResourceEventsQuery) Descriptor() ([]byte, []int) { + return fileDescriptor_eacb9df0ce5738fa, []int{2} +} +func (m *ApplicationSetResourceEventsQuery) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ApplicationSetResourceEventsQuery) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ApplicationSetResourceEventsQuery.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ApplicationSetResourceEventsQuery) XXX_Merge(src proto.Message) { + xxx_messageInfo_ApplicationSetResourceEventsQuery.Merge(m, src) +} +func (m *ApplicationSetResourceEventsQuery) XXX_Size() int { + return m.Size() +} +func (m *ApplicationSetResourceEventsQuery) XXX_DiscardUnknown() { + xxx_messageInfo_ApplicationSetResourceEventsQuery.DiscardUnknown(m) +} + +var xxx_messageInfo_ApplicationSetResourceEventsQuery proto.InternalMessageInfo + +func (m *ApplicationSetResourceEventsQuery) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *ApplicationSetResourceEventsQuery) GetAppsetNamespace() string { + if m != nil { + return m.AppsetNamespace + } + return "" +} + type ApplicationSetResponse struct { Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` Applicationset *v1alpha1.ApplicationSet `protobuf:"bytes,2,opt,name=applicationset,proto3" json:"applicationset,omitempty"` @@ -168,7 +225,7 @@ func (m *ApplicationSetResponse) Reset() { *m = ApplicationSetResponse{} func (m *ApplicationSetResponse) String() string { return proto.CompactTextString(m) } func (*ApplicationSetResponse) ProtoMessage() {} func (*ApplicationSetResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_eacb9df0ce5738fa, []int{2} + return fileDescriptor_eacb9df0ce5738fa, []int{3} } func (m *ApplicationSetResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -223,7 +280,7 @@ func (m *ApplicationSetCreateRequest) Reset() { *m = ApplicationSetCreat func (m *ApplicationSetCreateRequest) String() string { return proto.CompactTextString(m) } func (*ApplicationSetCreateRequest) ProtoMessage() {} func (*ApplicationSetCreateRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_eacb9df0ce5738fa, []int{3} + return fileDescriptor_eacb9df0ce5738fa, []int{4} } func (m *ApplicationSetCreateRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -279,7 +336,7 @@ func (m *ApplicationSetDeleteRequest) Reset() { *m = ApplicationSetDelet func (m *ApplicationSetDeleteRequest) String() string { return proto.CompactTextString(m) } func (*ApplicationSetDeleteRequest) ProtoMessage() {} func (*ApplicationSetDeleteRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_eacb9df0ce5738fa, []int{4} + return fileDescriptor_eacb9df0ce5738fa, []int{5} } func (m *ApplicationSetDeleteRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -325,6 +382,7 @@ func (m *ApplicationSetDeleteRequest) GetAppsetNamespace() string { func init() { proto.RegisterType((*ApplicationSetGetQuery)(nil), "applicationset.ApplicationSetGetQuery") proto.RegisterType((*ApplicationSetListQuery)(nil), "applicationset.ApplicationSetListQuery") + proto.RegisterType((*ApplicationSetResourceEventsQuery)(nil), "applicationset.ApplicationSetResourceEventsQuery") proto.RegisterType((*ApplicationSetResponse)(nil), "applicationset.ApplicationSetResponse") proto.RegisterType((*ApplicationSetCreateRequest)(nil), "applicationset.ApplicationSetCreateRequest") proto.RegisterType((*ApplicationSetDeleteRequest)(nil), "applicationset.ApplicationSetDeleteRequest") @@ -335,40 +393,45 @@ func init() { } var fileDescriptor_eacb9df0ce5738fa = []byte{ - // 526 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x94, 0xdf, 0x8a, 0x13, 0x31, - 0x14, 0xc6, 0xc9, 0x76, 0xad, 0xbb, 0x11, 0x14, 0x02, 0xee, 0xd6, 0x51, 0x6a, 0x99, 0x8b, 0xb5, - 0xae, 0x98, 0xd0, 0x7a, 0xa7, 0x57, 0xfe, 0x81, 0x45, 0x28, 0xa2, 0xb3, 0xe0, 0x85, 0x5e, 0x48, - 0x76, 0x7a, 0x98, 0x1d, 0x77, 0x3a, 0x89, 0x49, 0x3a, 0x20, 0x8b, 0x37, 0x82, 0x4f, 0xe0, 0x13, - 0xa8, 0x37, 0x82, 0xb7, 0x3e, 0x84, 0x97, 0x82, 0x2f, 0x20, 0xc5, 0x07, 0x91, 0xc9, 0xcc, 0xb4, - 0x3b, 0xa1, 0xdb, 0x0a, 0x76, 0xef, 0x72, 0x26, 0x99, 0x73, 0x7e, 0xf9, 0xf2, 0x9d, 0x83, 0x77, - 0x35, 0xa8, 0x0c, 0x14, 0xe3, 0x52, 0x26, 0x71, 0xc8, 0x4d, 0x2c, 0x52, 0x0d, 0xc6, 0x09, 0xa9, - 0x54, 0xc2, 0x08, 0x72, 0xb1, 0xfe, 0xd5, 0xbb, 0x16, 0x09, 0x11, 0x25, 0xc0, 0xb8, 0x8c, 0x19, - 0x4f, 0x53, 0x61, 0x8a, 0x9d, 0xe2, 0xb4, 0x37, 0x88, 0x62, 0x73, 0x38, 0x3e, 0xa0, 0xa1, 0x18, - 0x31, 0xae, 0x22, 0x21, 0x95, 0x78, 0x6d, 0x17, 0xb7, 0xc3, 0x21, 0xcb, 0xfa, 0x4c, 0x1e, 0x45, - 0xf9, 0x9f, 0xfa, 0x64, 0x2d, 0x96, 0xf5, 0x78, 0x22, 0x0f, 0x79, 0x8f, 0x45, 0x90, 0x82, 0xe2, - 0x06, 0x86, 0x45, 0x36, 0xff, 0x39, 0xde, 0xba, 0x3f, 0x3b, 0xb7, 0x0f, 0x66, 0x0f, 0xcc, 0xb3, - 0x31, 0xa8, 0xb7, 0x84, 0xe0, 0xf5, 0x94, 0x8f, 0xa0, 0x85, 0x3a, 0xa8, 0xbb, 0x19, 0xd8, 0x35, - 0xe9, 0xe2, 0x4b, 0x5c, 0x4a, 0x0d, 0xe6, 0x09, 0x1f, 0x81, 0x96, 0x3c, 0x84, 0xd6, 0x9a, 0xdd, - 0x76, 0x3f, 0xfb, 0xc7, 0x78, 0xbb, 0x9e, 0x77, 0x10, 0xeb, 0x32, 0xb1, 0x87, 0x37, 0x72, 0x66, - 0x08, 0x8d, 0x6e, 0xa1, 0x4e, 0xa3, 0xbb, 0x19, 0x4c, 0xe3, 0x7c, 0x4f, 0x43, 0x02, 0xa1, 0x11, - 0xaa, 0xcc, 0x3c, 0x8d, 0xe7, 0x15, 0x6f, 0xcc, 0x2f, 0xfe, 0x15, 0xb9, 0xb7, 0x0a, 0x40, 0xcb, - 0x5c, 0x5c, 0xd2, 0xc2, 0xe7, 0xcb, 0x62, 0xe5, 0xc5, 0xaa, 0x90, 0x18, 0xec, 0xbc, 0x83, 0x05, - 0xb8, 0xd0, 0x1f, 0xd0, 0x99, 0xe0, 0xb4, 0x12, 0xdc, 0x2e, 0x5e, 0x85, 0x43, 0x9a, 0xf5, 0xa9, - 0x3c, 0x8a, 0x68, 0x2e, 0x38, 0x3d, 0xf1, 0x3b, 0xad, 0x04, 0xa7, 0x0e, 0x87, 0x53, 0xc3, 0xff, - 0x86, 0xf0, 0xd5, 0xfa, 0x91, 0x87, 0x0a, 0xb8, 0x81, 0x00, 0xde, 0x8c, 0x41, 0xcf, 0xa3, 0x42, - 0x67, 0x4f, 0x45, 0xb6, 0x70, 0x73, 0x2c, 0x35, 0xa8, 0x42, 0x83, 0x8d, 0xa0, 0x8c, 0xfc, 0x97, - 0x2e, 0xec, 0x23, 0x48, 0x60, 0x06, 0xfb, 0x5f, 0x96, 0xe9, 0x7f, 0x3a, 0x87, 0x2f, 0xd7, 0xb3, - 0xef, 0x83, 0xca, 0xe2, 0x10, 0xc8, 0x17, 0x84, 0x1b, 0x7b, 0x60, 0xc8, 0x0e, 0x75, 0xfa, 0x67, - 0xbe, 0x75, 0xbd, 0x95, 0x8a, 0xe3, 0xef, 0xbc, 0xff, 0xf5, 0xe7, 0xe3, 0x5a, 0x87, 0xb4, 0x6d, - 0x43, 0x66, 0x3d, 0xa7, 0x89, 0x35, 0x3b, 0xce, 0x2f, 0xfa, 0x8e, 0x7c, 0x46, 0x78, 0x3d, 0x77, - 0x39, 0xb9, 0xb1, 0x18, 0x73, 0xda, 0x09, 0xde, 0xd3, 0x55, 0x72, 0xe6, 0x69, 0xfd, 0xeb, 0x96, - 0xf5, 0x0a, 0xd9, 0x3e, 0x85, 0x95, 0x7c, 0x47, 0xb8, 0x59, 0x38, 0x8c, 0xdc, 0x5a, 0x8c, 0x59, - 0xf3, 0xe1, 0x8a, 0x25, 0x65, 0x16, 0xf3, 0xa6, 0x7f, 0x1a, 0xe6, 0x5d, 0xd7, 0x90, 0x1f, 0x10, - 0x6e, 0x16, 0x5e, 0x5b, 0x86, 0x5d, 0x73, 0xa4, 0xb7, 0xc4, 0x31, 0xd5, 0x58, 0xa8, 0xde, 0x78, - 0x77, 0xc9, 0x1b, 0x3f, 0x78, 0xfc, 0x63, 0xd2, 0x46, 0x3f, 0x27, 0x6d, 0xf4, 0x7b, 0xd2, 0x46, - 0x2f, 0xee, 0xfd, 0xdb, 0x28, 0x0e, 0x93, 0x18, 0x52, 0x77, 0xf6, 0x1f, 0x34, 0xed, 0x00, 0xbe, - 0xf3, 0x37, 0x00, 0x00, 0xff, 0xff, 0x96, 0x3f, 0x16, 0xa7, 0x2a, 0x06, 0x00, 0x00, + // 607 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x95, 0xdf, 0x6a, 0x13, 0x4f, + 0x14, 0xc7, 0x99, 0xb6, 0xbf, 0xfc, 0xda, 0x11, 0x14, 0x06, 0x6c, 0xe3, 0xaa, 0xb1, 0x2e, 0x98, + 0xc6, 0x4a, 0x67, 0x48, 0xbc, 0x11, 0xbd, 0xf2, 0x1f, 0x45, 0x08, 0xa2, 0x5b, 0xf0, 0x42, 0x2f, + 0x64, 0xba, 0x39, 0x6c, 0xd7, 0x6c, 0x76, 0xc6, 0x99, 0xc9, 0x82, 0x14, 0x6f, 0x04, 0x9f, 0x40, + 0x7c, 0x01, 0xbd, 0x11, 0xbc, 0xf5, 0xc6, 0x37, 0xf0, 0x52, 0xf0, 0x05, 0x24, 0xf8, 0x20, 0xb2, + 0xb3, 0x9b, 0xa4, 0xbb, 0x6e, 0x13, 0xc5, 0x78, 0x37, 0x67, 0xe7, 0xec, 0x39, 0x9f, 0x39, 0xe7, + 0x7c, 0x67, 0xf0, 0xb6, 0x06, 0x95, 0x80, 0x62, 0x5c, 0xca, 0x28, 0xf4, 0xb9, 0x09, 0x45, 0xac, + 0xc1, 0x94, 0x4c, 0x2a, 0x95, 0x30, 0x82, 0x9c, 0x2c, 0x7e, 0x75, 0xce, 0x05, 0x42, 0x04, 0x11, + 0x30, 0x2e, 0x43, 0xc6, 0xe3, 0x58, 0x98, 0x6c, 0x27, 0xf3, 0x76, 0xdc, 0xfe, 0x35, 0x4d, 0x43, + 0x61, 0x77, 0x7d, 0xa1, 0x80, 0x25, 0x6d, 0x16, 0x40, 0x0c, 0x8a, 0x1b, 0xe8, 0xe5, 0x3e, 0xdd, + 0x20, 0x34, 0x07, 0xc3, 0x7d, 0xea, 0x8b, 0x01, 0xe3, 0x2a, 0x10, 0x52, 0x89, 0x67, 0x76, 0xb1, + 0xe3, 0xf7, 0x58, 0xd2, 0x61, 0xb2, 0x1f, 0xa4, 0xff, 0xeb, 0xa3, 0x3c, 0x2c, 0x69, 0xf3, 0x48, + 0x1e, 0xf0, 0x5f, 0xa2, 0xb9, 0x8f, 0xf0, 0xfa, 0xcd, 0xa9, 0xdf, 0x1e, 0x98, 0x5d, 0x30, 0x0f, + 0x87, 0xa0, 0x5e, 0x10, 0x82, 0x57, 0x62, 0x3e, 0x80, 0x3a, 0xda, 0x44, 0xad, 0x35, 0xcf, 0xae, + 0x49, 0x0b, 0x9f, 0xe2, 0x52, 0x6a, 0x30, 0xf7, 0xf9, 0x00, 0xb4, 0xe4, 0x3e, 0xd4, 0x97, 0xec, + 0x76, 0xf9, 0xb3, 0x7b, 0x88, 0x37, 0x8a, 0x71, 0xbb, 0xa1, 0xce, 0x03, 0x3b, 0x78, 0x35, 0x65, + 0x06, 0xdf, 0xe8, 0x3a, 0xda, 0x5c, 0x6e, 0xad, 0x79, 0x13, 0x3b, 0xdd, 0xd3, 0x10, 0x81, 0x6f, + 0x84, 0xca, 0x23, 0x4f, 0xec, 0xaa, 0xe4, 0xcb, 0xd5, 0xc9, 0x39, 0xbe, 0x58, 0x4c, 0xee, 0x81, + 0x16, 0x43, 0xe5, 0xc3, 0xdd, 0x04, 0x62, 0xa3, 0xff, 0xe8, 0x7c, 0xff, 0x55, 0xa7, 0xf8, 0x80, + 0xca, 0x85, 0xf3, 0x40, 0xcb, 0xb4, 0xc7, 0xa4, 0x8e, 0xff, 0xcf, 0xcf, 0x93, 0xc7, 0x1e, 0x9b, + 0xc4, 0xe0, 0xd2, 0x38, 0xd8, 0x33, 0x9e, 0xe8, 0x74, 0xe9, 0xb4, 0xa7, 0x74, 0xdc, 0x53, 0xbb, + 0x78, 0xea, 0xf7, 0x68, 0xd2, 0xa1, 0xb2, 0x1f, 0xd0, 0xb4, 0xa7, 0xf4, 0xc8, 0xef, 0x74, 0xdc, + 0x53, 0x5a, 0xe2, 0x28, 0xe5, 0x70, 0x3f, 0x22, 0x7c, 0xb6, 0xe8, 0x72, 0x5b, 0x01, 0x37, 0xe0, + 0xc1, 0xf3, 0x21, 0xe8, 0x2a, 0x2a, 0xf4, 0xef, 0xa9, 0xc8, 0x3a, 0xae, 0x0d, 0xa5, 0x06, 0x95, + 0xd5, 0x60, 0xd5, 0xcb, 0x2d, 0xf7, 0x49, 0x19, 0xf6, 0x0e, 0x44, 0x30, 0x85, 0xfd, 0xab, 0xa9, + 0xec, 0x7c, 0xae, 0xe1, 0xd3, 0xc5, 0xe8, 0x7b, 0xa0, 0x92, 0xd0, 0x07, 0xf2, 0x1e, 0xe1, 0xe5, + 0x5d, 0x30, 0xa4, 0x49, 0x4b, 0x32, 0xae, 0x56, 0x87, 0xb3, 0xd0, 0xe2, 0xb8, 0xcd, 0x57, 0xdf, + 0x7e, 0xbc, 0x59, 0xda, 0x24, 0x0d, 0xab, 0xfc, 0xa4, 0x5d, 0xba, 0x4b, 0x34, 0x3b, 0x4c, 0x0f, + 0xfa, 0x92, 0xbc, 0x43, 0x78, 0x25, 0x15, 0x12, 0xd9, 0x9a, 0x8d, 0x39, 0x11, 0x9b, 0xf3, 0x60, + 0x91, 0x9c, 0x69, 0x58, 0xf7, 0x82, 0x65, 0x3d, 0x43, 0x36, 0x8e, 0x61, 0x25, 0x6f, 0x11, 0x26, + 0xa9, 0x67, 0x51, 0x74, 0xa4, 0x3d, 0x1b, 0xb9, 0x42, 0xa2, 0xce, 0x79, 0x9a, 0xdd, 0x87, 0x29, + 0x20, 0x4d, 0xef, 0x43, 0x9a, 0xb4, 0xa9, 0x75, 0xb0, 0x24, 0x3b, 0x96, 0x64, 0x8b, 0x5c, 0x9a, + 0x5d, 0x35, 0x06, 0x19, 0xc0, 0x27, 0x84, 0x6b, 0xd9, 0xe4, 0x93, 0x2b, 0xb3, 0x59, 0x0a, 0xfa, + 0x58, 0x70, 0xab, 0x99, 0x85, 0xbe, 0xec, 0x1e, 0x57, 0xbe, 0xeb, 0x65, 0xa1, 0xbc, 0x46, 0xb8, + 0x96, 0x69, 0x60, 0x1e, 0x76, 0x41, 0x29, 0x4e, 0x73, 0x6e, 0xbd, 0xed, 0x75, 0x35, 0x9e, 0xbd, + 0xed, 0x39, 0xb3, 0x77, 0xeb, 0xde, 0x97, 0x51, 0x03, 0x7d, 0x1d, 0x35, 0xd0, 0xf7, 0x51, 0x03, + 0x3d, 0xbe, 0xf1, 0x7b, 0xaf, 0x90, 0x1f, 0x85, 0x10, 0x97, 0x9f, 0xc6, 0xfd, 0x9a, 0x7d, 0x7b, + 0xae, 0xfe, 0x0c, 0x00, 0x00, 0xff, 0xff, 0xb1, 0x98, 0xf7, 0x9c, 0x49, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -387,6 +450,8 @@ type ApplicationSetServiceClient interface { Get(ctx context.Context, in *ApplicationSetGetQuery, opts ...grpc.CallOption) (*v1alpha1.ApplicationSet, error) //List returns list of applicationset List(ctx context.Context, in *ApplicationSetListQuery, opts ...grpc.CallOption) (*v1alpha1.ApplicationSetList, error) + // ListResourceEvents returns a list of event resources + ListResourceEvents(ctx context.Context, in *ApplicationSetResourceEventsQuery, opts ...grpc.CallOption) (*v1.EventList, error) //Create creates an applicationset Create(ctx context.Context, in *ApplicationSetCreateRequest, opts ...grpc.CallOption) (*v1alpha1.ApplicationSet, error) // Delete deletes an application set @@ -419,6 +484,15 @@ func (c *applicationSetServiceClient) List(ctx context.Context, in *ApplicationS return out, nil } +func (c *applicationSetServiceClient) ListResourceEvents(ctx context.Context, in *ApplicationSetResourceEventsQuery, opts ...grpc.CallOption) (*v1.EventList, error) { + out := new(v1.EventList) + err := c.cc.Invoke(ctx, "/applicationset.ApplicationSetService/ListResourceEvents", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *applicationSetServiceClient) Create(ctx context.Context, in *ApplicationSetCreateRequest, opts ...grpc.CallOption) (*v1alpha1.ApplicationSet, error) { out := new(v1alpha1.ApplicationSet) err := c.cc.Invoke(ctx, "/applicationset.ApplicationSetService/Create", in, out, opts...) @@ -443,6 +517,8 @@ type ApplicationSetServiceServer interface { Get(context.Context, *ApplicationSetGetQuery) (*v1alpha1.ApplicationSet, error) //List returns list of applicationset List(context.Context, *ApplicationSetListQuery) (*v1alpha1.ApplicationSetList, error) + // ListResourceEvents returns a list of event resources + ListResourceEvents(context.Context, *ApplicationSetResourceEventsQuery) (*v1.EventList, error) //Create creates an applicationset Create(context.Context, *ApplicationSetCreateRequest) (*v1alpha1.ApplicationSet, error) // Delete deletes an application set @@ -459,6 +535,9 @@ func (*UnimplementedApplicationSetServiceServer) Get(ctx context.Context, req *A func (*UnimplementedApplicationSetServiceServer) List(ctx context.Context, req *ApplicationSetListQuery) (*v1alpha1.ApplicationSetList, error) { return nil, status.Errorf(codes.Unimplemented, "method List not implemented") } +func (*UnimplementedApplicationSetServiceServer) ListResourceEvents(ctx context.Context, req *ApplicationSetResourceEventsQuery) (*v1.EventList, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListResourceEvents not implemented") +} func (*UnimplementedApplicationSetServiceServer) Create(ctx context.Context, req *ApplicationSetCreateRequest) (*v1alpha1.ApplicationSet, error) { return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") } @@ -506,6 +585,24 @@ func _ApplicationSetService_List_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _ApplicationSetService_ListResourceEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplicationSetResourceEventsQuery) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApplicationSetServiceServer).ListResourceEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/applicationset.ApplicationSetService/ListResourceEvents", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApplicationSetServiceServer).ListResourceEvents(ctx, req.(*ApplicationSetResourceEventsQuery)) + } + return interceptor(ctx, in, info, handler) +} + func _ApplicationSetService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ApplicationSetCreateRequest) if err := dec(in); err != nil { @@ -554,6 +651,10 @@ var _ApplicationSetService_serviceDesc = grpc.ServiceDesc{ MethodName: "List", Handler: _ApplicationSetService_List_Handler, }, + { + MethodName: "ListResourceEvents", + Handler: _ApplicationSetService_ListResourceEvents_Handler, + }, { MethodName: "Create", Handler: _ApplicationSetService_Create_Handler, @@ -658,6 +759,47 @@ func (m *ApplicationSetListQuery) MarshalToSizedBuffer(dAtA []byte) (int, error) return len(dAtA) - i, nil } +func (m *ApplicationSetResourceEventsQuery) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ApplicationSetResourceEventsQuery) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ApplicationSetResourceEventsQuery) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.AppsetNamespace) > 0 { + i -= len(m.AppsetNamespace) + copy(dAtA[i:], m.AppsetNamespace) + i = encodeVarintApplicationset(dAtA, i, uint64(len(m.AppsetNamespace))) + i-- + dAtA[i] = 0x2a + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintApplicationset(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *ApplicationSetResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -851,6 +993,26 @@ func (m *ApplicationSetListQuery) Size() (n int) { return n } +func (m *ApplicationSetResourceEventsQuery) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + sovApplicationset(uint64(l)) + } + l = len(m.AppsetNamespace) + if l > 0 { + n += 1 + l + sovApplicationset(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func (m *ApplicationSetResponse) Size() (n int) { if m == nil { return 0 @@ -1178,6 +1340,121 @@ func (m *ApplicationSetListQuery) Unmarshal(dAtA []byte) error { } return nil } +func (m *ApplicationSetResourceEventsQuery) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApplicationset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ApplicationSetResourceEventsQuery: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ApplicationSetResourceEventsQuery: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApplicationset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthApplicationset + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthApplicationset + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AppsetNamespace", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApplicationset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthApplicationset + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthApplicationset + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AppsetNamespace = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipApplicationset(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthApplicationset + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *ApplicationSetResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/pkg/apiclient/applicationset/applicationset.pb.gw.go b/pkg/apiclient/applicationset/applicationset.pb.gw.go index 5e4c73f7add3b..4ae82a36fcd4c 100644 --- a/pkg/apiclient/applicationset/applicationset.pb.gw.go +++ b/pkg/apiclient/applicationset/applicationset.pb.gw.go @@ -141,6 +141,78 @@ func local_request_ApplicationSetService_List_0(ctx context.Context, marshaler r } +var ( + filter_ApplicationSetService_ListResourceEvents_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} +) + +func request_ApplicationSetService_ListResourceEvents_0(ctx context.Context, marshaler runtime.Marshaler, client ApplicationSetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApplicationSetResourceEventsQuery + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + + protoReq.Name, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ApplicationSetService_ListResourceEvents_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListResourceEvents(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_ApplicationSetService_ListResourceEvents_0(ctx context.Context, marshaler runtime.Marshaler, server ApplicationSetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ApplicationSetResourceEventsQuery + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + + protoReq.Name, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ApplicationSetService_ListResourceEvents_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ListResourceEvents(ctx, &protoReq) + return msg, metadata, err + +} + var ( filter_ApplicationSetService_Create_0 = &utilities.DoubleArray{Encoding: map[string]int{"applicationset": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) @@ -317,6 +389,29 @@ func RegisterApplicationSetServiceHandlerServer(ctx context.Context, mux *runtim }) + mux.Handle("GET", pattern_ApplicationSetService_ListResourceEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ApplicationSetService_ListResourceEvents_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_ApplicationSetService_ListResourceEvents_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ApplicationSetService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -444,6 +539,26 @@ func RegisterApplicationSetServiceHandlerClient(ctx context.Context, mux *runtim }) + mux.Handle("GET", pattern_ApplicationSetService_ListResourceEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ApplicationSetService_ListResourceEvents_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_ApplicationSetService_ListResourceEvents_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_ApplicationSetService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -492,6 +607,8 @@ var ( pattern_ApplicationSetService_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "applicationsets"}, "", runtime.AssumeColonVerbOpt(true))) + pattern_ApplicationSetService_ListResourceEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "applicationsets", "name", "events"}, "", runtime.AssumeColonVerbOpt(true))) + pattern_ApplicationSetService_Create_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "applicationsets"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ApplicationSetService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "applicationsets", "name"}, "", runtime.AssumeColonVerbOpt(true))) @@ -502,6 +619,8 @@ var ( forward_ApplicationSetService_List_0 = runtime.ForwardResponseMessage + forward_ApplicationSetService_ListResourceEvents_0 = runtime.ForwardResponseMessage + forward_ApplicationSetService_Create_0 = runtime.ForwardResponseMessage forward_ApplicationSetService_Delete_0 = runtime.ForwardResponseMessage diff --git a/server/applicationset/applicationset.go b/server/applicationset/applicationset.go index d67815bd9a53d..fc3982bcc5d0e 100644 --- a/server/applicationset/applicationset.go +++ b/server/applicationset/applicationset.go @@ -16,6 +16,7 @@ import ( v1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" @@ -39,6 +40,7 @@ type Server struct { ns string db db.ArgoDB enf *rbac.Enforcer + kubeclientset kubernetes.Interface appclientset appclientset.Interface appsetInformer cache.SharedIndexInformer appsetLister applisters.ApplicationSetLister @@ -67,6 +69,7 @@ func NewServer( ns: namespace, db: db, enf: enf, + kubeclientset: kubeclientset, appclientset: appclientset, appsetInformer: appsetInformer, appsetLister: appsetLister, @@ -280,6 +283,38 @@ func (s *Server) Delete(ctx context.Context, q *applicationset.ApplicationSetDel } +// ListResourceEvents returns a list of event resources +func (s *Server) ListResourceEvents(ctx context.Context, q *applicationset.ApplicationSetResourceEventsQuery) (*v1.EventList, error) { + namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace) + + if !s.isNamespaceEnabled(namespace) { + return nil, security.NamespaceNotPermittedError(namespace) + } + + a, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, q.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error getting ApplicationSet: %w", err) + } + + if err = s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionGet, a.RBACName(s.ns)); err != nil { + return nil, err + } + + fieldSelector := fields.SelectorFromSet(map[string]string{ + "involvedObject.name": a.Name, + "involvedObject.uid": string(a.UID), + "involvedObject.namespace": a.Namespace, + }).String() + + log.Infof("Querying for resource events with field selector: %s", fieldSelector) + opts := metav1.ListOptions{FieldSelector: fieldSelector} + list, err := s.kubeclientset.CoreV1().Events(namespace).List(ctx, opts) + if err != nil { + return nil, fmt.Errorf("error listing resource events: %w", err) + } + return list, nil +} + func (s *Server) validateAppSet(ctx context.Context, appset *v1alpha1.ApplicationSet) (string, error) { if appset == nil { return "", fmt.Errorf("ApplicationSet cannot be validated for nil value") diff --git a/server/applicationset/applicationset.proto b/server/applicationset/applicationset.proto index 2a857d41a00ce..49f058b7c7a90 100644 --- a/server/applicationset/applicationset.proto +++ b/server/applicationset/applicationset.proto @@ -8,6 +8,7 @@ option go_package = "github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset package applicationset; import "google/api/annotations.proto"; +import "k8s.io/api/core/v1/generated.proto"; import "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1/generated.proto"; // ApplicationSetGetQuery is a query for applicationset resources @@ -28,6 +29,13 @@ message ApplicationSetListQuery { } +// ApplicationSetEventsQuery is a query for applicationset resource events +message ApplicationSetResourceEventsQuery { + string name = 1; + string appsetNamespace = 5; +} + + message ApplicationSetResponse { string project = 1; github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.ApplicationSet applicationset = 2; @@ -60,6 +68,11 @@ service ApplicationSetService { option (google.api.http).get = "/api/v1/applicationsets"; } + // ListResourceEvents returns a list of event resources + rpc ListResourceEvents(ApplicationSetResourceEventsQuery) returns (k8s.io.api.core.v1.EventList) { + option (google.api.http).get = "/api/v1/applicationsets/{name}/events"; + } + //Create creates an applicationset rpc Create (ApplicationSetCreateRequest) returns (github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.ApplicationSet) { option (google.api.http) = { @@ -73,4 +86,4 @@ service ApplicationSetService { option (google.api.http).delete = "/api/v1/applicationsets/{name}"; } -} \ No newline at end of file +} diff --git a/server/applicationset/applicationset_test.go b/server/applicationset/applicationset_test.go index c49ddb35a7970..ed4012728ede4 100644 --- a/server/applicationset/applicationset_test.go +++ b/server/applicationset/applicationset_test.go @@ -51,7 +51,17 @@ func newTestAppSetServer(objects ...runtime.Object) *Server { enf.SetDefaultRole("role:admin") } scopedNamespaces := "" - return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, objects...) + return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, []runtime.Object{}, objects...) +} + +// return an ApplicationServiceServer which returns fake data +func newTestAppSetServerWithKubeObjects(kubeObjects []runtime.Object, objects ...runtime.Object) *Server { + f := func(enf *rbac.Enforcer) { + _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) + enf.SetDefaultRole("role:admin") + } + scopedNamespaces := "" + return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, kubeObjects, objects...) } // return an ApplicationServiceServer which returns fake data @@ -61,11 +71,11 @@ func newTestNamespacedAppSetServer(objects ...runtime.Object) *Server { enf.SetDefaultRole("role:admin") } scopedNamespaces := "argocd" - return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, objects...) + return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, []runtime.Object{}, objects...) } -func newTestAppSetServerWithEnforcerConfigure(f func(*rbac.Enforcer), namespace string, objects ...runtime.Object) *Server { - kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ +func newTestAppSetServerWithEnforcerConfigure(f func(*rbac.Enforcer), namespace string, kubeObjects []runtime.Object, objects ...runtime.Object) *Server { + kubeObjects = append(kubeObjects, &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNamespace, Name: "argocd-cm", @@ -83,6 +93,7 @@ func newTestAppSetServerWithEnforcerConfigure(f func(*rbac.Enforcer), namespace "server.secretkey": []byte("test"), }, }) + kubeclientset := fake.NewSimpleClientset(kubeObjects...) ctx := context.Background() db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset) _, err := db.CreateRepository(ctx, fakeRepo()) @@ -474,3 +485,67 @@ func TestUpdateAppSet(t *testing.T) { }) } + +func TestListResourceEventsAppSet(t *testing.T) { + appSet := newTestAppSet(func(appset *appsv1.ApplicationSet) { + appset.Name = "AppSet1" + }) + + event1 := &v1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Name: "event1", + Namespace: testNamespace, + }, + InvolvedObject: v1.ObjectReference{ + Name: appSet.Name, + Namespace: testNamespace, + UID: appSet.UID, + }, + } + + event2 := &v1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Name: "event2", + Namespace: "other-namespace", + }, + InvolvedObject: v1.ObjectReference{ + Name: appSet.Name, + Namespace: "other-namespace", + UID: appSet.UID, + }, + } + + t.Run("List events in default namespace", func(t *testing.T) { + + appSetServer := newTestAppSetServerWithKubeObjects([]runtime.Object{event1, event2}, appSet) + + q := applicationset.ApplicationSetResourceEventsQuery{Name: "AppSet1"} + + res, err := appSetServer.ListResourceEvents(context.Background(), &q) + assert.NoError(t, err) + assert.Len(t, res.Items, 1) + assert.Equal(t, res.Items[0].Message, event1.Message) + }) + + t.Run("List events in named namespace", func(t *testing.T) { + + appSetServer := newTestAppSetServerWithKubeObjects([]runtime.Object{event1, event2}, appSet) + + q := applicationset.ApplicationSetResourceEventsQuery{Name: "AppSet1", AppsetNamespace: testNamespace} + + res, err := appSetServer.ListResourceEvents(context.Background(), &q) + assert.NoError(t, err) + assert.Len(t, res.Items, 1) + assert.Equal(t, res.Items[0].Message, event1.Message) + }) + + t.Run("List events in not allowed namespace", func(t *testing.T) { + + appSetServer := newTestAppSetServerWithKubeObjects([]runtime.Object{event1, event2}, appSet) + + q := applicationset.ApplicationSetResourceEventsQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"} + + _, err := appSetServer.ListResourceEvents(context.Background(), &q) + assert.Equal(t, "namespace 'NOT-ALLOWED' is not permitted", err.Error()) + }) +} diff --git a/ui/src/app/app.tsx b/ui/src/app/app.tsx index d0a58d3fbdc7f..74e71dad56f28 100644 --- a/ui/src/app/app.tsx +++ b/ui/src/app/app.tsx @@ -31,6 +31,7 @@ type Routes = {[path: string]: {component: React.ComponentType { } /> - services.applications.list([], {fields: ['items.metadata.name']})}> + services.applications.list([], ctx, {fields: ['items.metadata.name']})}> {apps => apps.items .filter(app => { diff --git a/ui/src/app/applications/components/application-details/application-details.tsx b/ui/src/app/applications/components/application-details/application-details.tsx index a3e8175591dde..3eb16ccfdaa0c 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -11,7 +11,15 @@ import {delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators'; import {DataLoader, EmptyState, ErrorNotification, ObservableQuery, Page, Paginate, Revision, Timestamp} from '../../../shared/components'; import {AppContext, ContextApis} from '../../../shared/context'; import * as appModels from '../../../shared/models'; -import {AppDetailsPreferences, AppsDetailsViewKey, AppsDetailsViewType, services} from '../../../shared/services'; +import { + AbstractAppDetailsPreferences, + AppDetailsPreferences, + AppSetsDetailsViewKey, + AppSetsDetailsViewType, + AppsDetailsViewKey, + AppsDetailsViewType, + services +} from '../../../shared/services'; import {ApplicationConditions} from '../application-conditions/application-conditions'; import {ApplicationDeploymentHistory} from '../application-deployment-history/application-deployment-history'; @@ -23,9 +31,9 @@ import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-p import {ResourceDetails} from '../resource-details/resource-details'; import * as AppUtils from '../utils'; import {ApplicationResourceList} from './application-resource-list'; -import {Filters, FiltersProps} from './application-resource-filter'; -import {getAppDefaultSource, urlPattern, helpTip} from '../utils'; -import {ChartDetails, ResourceStatus} from '../../../shared/models'; +import {AbstractFiltersProps, Filters} from './application-resource-filter'; +import {getAppDefaultSource, urlPattern, helpTip, isApp, isInvokedFromAppsPath} from '../utils'; +import {AbstractApplication, ApplicationTree, ChartDetails, ResourceStatus} from '../../../shared/models'; import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown'; import {useSidebarTarget} from '../../../sidebar/sidebar'; @@ -54,7 +62,7 @@ interface FilterInput { namespace: string[]; } -const ApplicationDetailsFilters = (props: FiltersProps) => { +const ApplicationDetailsFilters = (props: AbstractFiltersProps) => { const sidebarTarget = useSidebarTarget(); return ReactDOM.createPortal(, sidebarTarget?.current); }; @@ -79,7 +87,7 @@ export class ApplicationDetails extends React.Component(null); + private appChanged = new BehaviorSubject(null); private appNamespace: string; constructor(props: RouteComponentProps<{appnamespace: string; name: string}>) { @@ -160,22 +168,36 @@ export class ApplicationDetails extends React.Component (usrMsg.appName === appName && usrMsg.msgKey === 'groupNodes' ? {...usrMsg, display: true} : usrMsg)); + private toggleCompactView(app: AbstractApplication, pref: AbstractAppDetailsPreferences) { + if (isApp(app)) { + (pref as AppDetailsPreferences).userHelpTipMsgs = (pref as AppDetailsPreferences).userHelpTipMsgs.map(usrMsg => + usrMsg.appName === app.metadata.name && usrMsg.msgKey === 'groupNodes' ? {...usrMsg, display: true} : usrMsg + ); + } services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: !pref.groupNodes}}); } - private getPageTitle(view: string) { - const {Tree, Pods, Network, List} = AppsDetailsViewKey; - switch (view) { - case Tree: - return 'Application Details Tree'; - case Network: - return 'Application Details Network'; - case Pods: - return 'Application Details Pods'; - case List: - return 'Application Details List'; + private getPageTitle(view: string, isAnApp: boolean) { + if (isAnApp) { + const {Tree, Pods, Network, List} = AppsDetailsViewKey; + switch (view) { + case Tree: + return 'Application Details Tree'; + case Network: + return 'Application Details Network'; + case Pods: + return 'Application Details Pods'; + case List: + return 'Application Details List'; + } + } else { + const {Tree, List} = AppSetsDetailsViewKey; + switch (view) { + case Tree: + return 'ApplicationSet Details Tree'; + case List: + return 'ApplicationSet Details List'; + } } return ''; } @@ -185,8 +207,12 @@ export class ApplicationDetails extends React.Component {q => ( {error}} - loadingRenderer={() => Loading...} + errorRenderer={error => ( + {error} + )} + loadingRenderer={() => ( + Loading... + )} input={this.props.match.params.name} load={name => combineLatest([this.loadAppInfo(name, this.appNamespace), services.viewPreferences.getPreferences(), q]).pipe( @@ -201,11 +227,15 @@ export class ApplicationDetails extends React.Component !!item); } if (params.get('view') != null) { - pref.view = params.get('view') as AppsDetailsViewType; + pref.view = isApp(application) ? (params.get('view') as AppsDetailsViewType) : (params.get('view') as AppSetsDetailsViewType); } else { - const appDefaultView = (application.metadata && - application.metadata.annotations && - application.metadata.annotations[appModels.AnnotationDefaultView]) as AppsDetailsViewType; + const appDefaultView = isApp(application) + ? ((application.metadata && + application.metadata.annotations && + application.metadata.annotations[appModels.AnnotationDefaultView]) as AppsDetailsViewType) + : ((application.metadata && + application.metadata.annotations && + application.metadata.annotations[appModels.AnnotationDefaultView]) as AppSetsDetailsViewType); if (appDefaultView != null) { pref.view = appDefaultView; } @@ -213,21 +243,29 @@ export class ApplicationDetails extends React.Component - {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => { + {({ + application, + tree, + pref + }: { + application: appModels.AbstractApplication; + tree: appModels.AbstractApplicationTree; + pref: AbstractAppDetailsPreferences; + }) => { tree.nodes = tree.nodes || []; const treeFilter = this.getTreeFilter(pref.resourceFilter); const setFilter = (items: string[]) => { @@ -240,19 +278,28 @@ export class ApplicationDetails extends React.Component usrMsg.appName === application.metadata.name); + const source = isApp(application) ? getAppDefaultSource(application as models.Application) : null; + const showToolTip = isApp(application) + ? (pref as AppDetailsPreferences)?.userHelpTipMsgs.find(usrMsg => usrMsg.appName === application.metadata.name) + : null; const resourceNodes = (): any[] => { const statusByKey = new Map(); - application.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res)); + if (isApp(application)) { + (application as models.Application).status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res)); + } const resources = new Map(); tree.nodes .map(node => ({...node, orphaned: false})) - .concat(((pref.orphanedResources && tree.orphanedNodes) || []).map(node => ({...node, orphaned: true}))) + .concat( + ((pref.orphanedResources && isApp(application) ? (tree as models.ApplicationTree).orphanedNodes : []) || []).map(node => ({ + ...node, + orphaned: true + })) + ) .forEach(node => { const resource: any = {...node}; resource.uid = node.uid; @@ -312,11 +359,15 @@ export class ApplicationDetails extends React.Component { - const existingIndex = pref.userHelpTipMsgs.findIndex(msg => msg.appName === usrHelpTip.appName && msg.msgKey === usrHelpTip.msgKey); - if (existingIndex !== -1) { - pref.userHelpTipMsgs[existingIndex] = usrHelpTip; - } else { - (pref.userHelpTipMsgs || []).push(usrHelpTip); + if (isApp(application)) { + const existingIndex = (pref as AppDetailsPreferences).userHelpTipMsgs.findIndex( + msg => msg.appName === usrHelpTip.appName && msg.msgKey === usrHelpTip.msgKey + ); + if (existingIndex !== -1) { + (pref as AppDetailsPreferences).userHelpTipMsgs[existingIndex] = usrHelpTip; + } else { + ((pref as AppDetailsPreferences).userHelpTipMsgs || []).push(usrHelpTip); + } } }; const toggleNameDirection = () => { @@ -329,7 +380,7 @@ export class ApplicationDetails extends React.Component(); tree.nodes .map(node => ({...node, orphaned: false})) - .concat((tree.orphanedNodes || []).map(node => ({...node, orphaned: true}))) + .concat((isApp(application) ? (tree as models.ApplicationTree).orphanedNodes : [] || []).map(node => ({...node, orphaned: true}))) .forEach(node => { const resourceNode: ResourceTreeNode = {...node}; nodes.push(resourceNode); @@ -372,12 +423,12 @@ export class ApplicationDetails extends React.Component } ], actionMenu: {items: this.getApplicationActionMenu(application, true)}, @@ -392,22 +443,26 @@ export class ApplicationDetails extends React.Component - { - this.appContext.apis.navigation.goto('.', {view: Pods}); - services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}}); - }} - /> - { - this.appContext.apis.navigation.goto('.', {view: Network}); - services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}}); - }} - /> + {isApp(application) && ( + { + this.appContext.apis.navigation.goto('.', {view: Pods}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}}); + }} + /> + )} + {isApp(application) && ( + { + this.appContext.apis.navigation.goto('.', {view: Network}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}}); + }} + /> + )} this.toggleCompactView(application.metadata.name, pref)}> + onClick={() => this.toggleCompactView(application, pref)}> @@ -511,12 +566,18 @@ export class ApplicationDetails extends React.Component this.selectNode(fullName)} nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => - this.getApplicationActionMenu(application, false) + AppUtils.renderResourceMenu( + node, + application, + tree as ApplicationTree, + this.appContext.apis, + this.props.history, + this.appChanged, + () => this.getApplicationActionMenu(application, false) ) } showCompactNodes={pref.groupNodes} - userMsgs={pref.userHelpTipMsgs} + userMsgs={isApp(application) ? (pref as AppDetailsPreferences).userHelpTipMsgs : []} tree={tree} app={application} showOrphanedResources={pref.orphanedResources} @@ -524,7 +585,7 @@ export class ApplicationDetails extends React.Component openGroupNodeDetails(groupdedNodeIds)} zoom={pref.zoom} - podGroupCount={pref.podGroupCount} + // podGroupCount={isApp(application) ? (pref as AppDetailsPreferences).podGroupCount : 0} appContext={this.appContext} nameDirection={this.state.truncateNameOnRight} filters={pref.resourceFilter} @@ -538,19 +599,34 @@ export class ApplicationDetails extends React.Component this.selectNode(fullName)} nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => - this.getApplicationActionMenu(application, false) + AppUtils.renderResourceMenu( + node, + application, + tree as ApplicationTree, + this.appContext.apis, + this.props.history, + this.appChanged, + () => this.getApplicationActionMenu(application, false) + ) + } + quickStarts={node => + AppUtils.renderResourceButtons( + node, + application, + tree as ApplicationTree, + this.appContext.apis, + this.props.history, + this.appChanged ) } - quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)} /> )) || (this.state.extensionsMap[pref.view] != null && ( - + )) || (
services.viewPreferences.getPreferences()}> @@ -579,13 +655,14 @@ export class ApplicationDetails extends React.Component this.getApplicationActionMenu(application, false) ) } - tree={tree} + tree={tree as ApplicationTree} /> )} @@ -611,11 +688,17 @@ export class ApplicationDetails extends React.Component this.selectNode(fullName)} resources={data} nodeMenu={node => - AppUtils.renderResourceMenu({...node, root: node}, application, tree, this.appContext.apis, this.appChanged, () => - this.getApplicationActionMenu(application, false) + AppUtils.renderResourceMenu( + {...node, root: node}, + application, + tree as ApplicationTree, + this.appContext.apis, + this.props.history, + this.appChanged, + () => this.getApplicationActionMenu(application, false) ) } - tree={tree} + tree={tree as ApplicationTree} /> )} @@ -623,7 +706,7 @@ export class ApplicationDetails extends React.Component this.selectNode('')}> this.updateApp(app, query)} @@ -766,80 +849,89 @@ export class ApplicationDetails extends React.Component {prop.actionLabel}; - const hasMultipleSources = app.spec.sources && app.spec.sources.length > 0; - return [ - { - iconClassName: 'fa fa-info-circle', - title: , - action: () => this.selectNode(fullName) - }, - { - iconClassName: 'fa fa-file-medical', - title: , - action: () => this.selectNode(fullName, 0, 'diff'), - disabled: app.status.sync.status === appModels.SyncStatuses.Synced - }, - { - iconClassName: 'fa fa-sync', - title: , - action: () => AppUtils.showDeploy('all', null, this.appContext.apis) - }, - { - iconClassName: 'fa fa-info-circle', - title: , - action: () => this.setOperationStatusVisible(true), - disabled: !app.status.operationState - }, - { - iconClassName: 'fa fa-history', - title: hasMultipleSources ? ( - - - {helpTip('Rollback is not supported for apps with multiple sources')} - - ) : ( - - ), - action: () => { - this.setRollbackPanelVisible(0); - }, - disabled: !app.status.operationState || hasMultipleSources - }, - { - iconClassName: 'fa fa-times-circle', - title: , - action: () => this.deleteApplication() - }, - { - iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}), - title: ( - - {' '} - !refreshing && services.applications.get(app.metadata.name, app.metadata.namespace, 'hard') - } - ]} - anchor={() => } - /> - - ), - disabled: !!refreshing, - action: () => { - if (!refreshing) { - services.applications.get(app.metadata.name, app.metadata.namespace, 'normal'); - AppUtils.setAppRefreshing(app); - this.appChanged.next(app); - } - } - } - ]; + const hasMultipleSources = isApp(app) ? app.spec.sources && app.spec.sources.length > 0 : false; + return isApp(app) + ? [ + { + iconClassName: 'fa fa-info-circle', + title: , + action: () => this.selectNode(fullName) + }, + { + iconClassName: 'fa fa-file-medical', + title: , + action: () => this.selectNode(fullName, 0, 'diff'), + disabled: (app as models.Application).status.sync.status === appModels.SyncStatuses.Synced + }, + { + iconClassName: 'fa fa-sync', + title: , + action: () => AppUtils.showDeploy('all', null, this.appContext.apis) + }, + { + iconClassName: 'fa fa-info-circle', + title: , + action: () => this.setOperationStatusVisible(true), + disabled: !(app as models.Application).status.operationState + }, + { + iconClassName: 'fa fa-history', + title: hasMultipleSources ? ( + + + {helpTip('Rollback is not supported for apps with multiple sources')} + + ) : ( + + ), + action: () => { + this.setRollbackPanelVisible(0); + }, + disabled: !(app as models.Application).status.operationState || hasMultipleSources + }, + { + iconClassName: 'fa fa-times-circle', + title: , + action: () => this.deleteApplication() + }, + { + iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}), + title: ( + + {' '} + + !refreshing && services.applications.get(app.metadata.name, app.metadata.namespace, this.props.history.location.pathname, 'hard') + } + ]} + anchor={() => } + /> + + ), + disabled: !!refreshing, + action: () => { + if (!refreshing) { + services.applications.get(app.metadata.name, app.metadata.namespace, this.props.location.pathname, 'normal'); + AppUtils.setAppRefreshing(app); + this.appChanged.next(app); + } + } + } + ] + : [ + { + iconClassName: 'fa fa-info-circle', + title: , + action: () => this.selectNode(fullName) + } + ]; } private filterTreeNode(node: ResourceTreeNode, filterInput: FilterInput): boolean { @@ -883,22 +975,24 @@ export class ApplicationDetails extends React.Component { - return from(services.applications.get(name, appNamespace)) + private loadAppInfo(name: string, appNamespace: string): Observable<{application: appModels.AbstractApplication; tree: appModels.AbstractApplicationTree}> { + return from(services.applications.get(name, appNamespace, this.props.history.location.pathname)) .pipe( mergeMap(app => { const fallbackTree = { - nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})), + nodes: isApp(app) + ? (app as models.Application).status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})) + : (app as models.ApplicationSet).status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})), orphanedNodes: [], hosts: [] - } as appModels.ApplicationTree; + } as appModels.AbstractApplicationTree; return combineLatest( merge( from([app]), this.appChanged.pipe(filter(item => !!item)), AppUtils.handlePageVisibility(() => services.applications - .watch({name, appNamespace}) + .watch(this.props.history.location.pathname, {name, appNamespace}) .pipe( map(watchEvent => { if (watchEvent.type === 'DELETED') { @@ -913,10 +1007,10 @@ export class ApplicationDetails extends React.Component fallbackTree), + services.applications.resourceTree(name, appNamespace, this.props.history.location.pathname).catch(() => fallbackTree), AppUtils.handlePageVisibility(() => services.applications - .watchResourceTree(name, appNamespace) + .watchResourceTree(name, appNamespace, this.props.history.location.pathname) .pipe(repeat()) .pipe(retryWhen(errors => errors.pipe(delay(500)))) ) @@ -934,7 +1028,7 @@ export class ApplicationDetails extends React.Component(); - tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(AppUtils.nodeKey(node), node)); + private groupAppNodesByKey(application: appModels.AbstractApplication, tree: appModels.AbstractApplicationTree) { + const nodeByKey = new Map(); + tree.nodes.concat((isApp(application) ? (tree as appModels.ApplicationTree).orphanedNodes : []) || []).forEach(node => nodeByKey.set(AppUtils.nodeKey(node), node)); nodeByKey.set(AppUtils.nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application); return nodeByKey; } @@ -1015,7 +1109,7 @@ Are you sure you want to disable auto-sync and rollback application '${this.prop await services.applications.update(update); } await services.applications.rollback(this.props.match.params.name, this.appNamespace, revisionHistory.id); - this.appChanged.next(await services.applications.get(this.props.match.params.name, this.appNamespace)); + this.appChanged.next(await services.applications.get(this.props.match.params.name, this.appNamespace, this.props.history.location.pathname)); this.setRollbackPanelVisible(-1); } } catch (e) { diff --git a/ui/src/app/applications/components/application-details/application-resource-filter.tsx b/ui/src/app/applications/components/application-details/application-resource-filter.tsx index a3d99f92488f3..d95037e1e2014 100644 --- a/ui/src/app/applications/components/application-details/application-resource-filter.tsx +++ b/ui/src/app/applications/components/application-details/application-resource-filter.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import {Checkbox} from 'argo-ui/v2'; -import {ApplicationTree, HealthStatusCode, HealthStatuses, SyncStatusCode, SyncStatuses} from '../../../shared/models'; -import {AppDetailsPreferences, services} from '../../../shared/services'; +import {AbstractApplicationTree, ApplicationTree, HealthStatusCode, HealthStatuses, SyncStatusCode, SyncStatuses} from '../../../shared/models'; +import {AppDetailsPreferences, AppSetDetailsPreferences, services} from '../../../shared/services'; import {Context} from '../../../shared/context'; import {Filter, FiltersGroup} from '../filter/filter'; -import {ComparisonStatusIcon, HealthStatusIcon} from '../utils'; +import {ComparisonStatusIcon, HealthStatusIcon, isInvokedFromAppsPath} from '../utils'; import {resources} from '../resources'; import * as models from '../../../shared/models'; @@ -14,17 +14,26 @@ function toOption(label: string) { return {label}; } -export interface FiltersProps { +export interface AbstractFiltersProps { children?: React.ReactNode; - pref: AppDetailsPreferences; - tree: ApplicationTree; + pref: AppDetailsPreferences | AppSetDetailsPreferences; + tree: AbstractApplicationTree; // | ApplicationSetTree; resourceNodes: models.ResourceStatus[]; onSetFilter: (items: string[]) => void; onClearFilter: () => void; collapsed?: boolean; } -export const Filters = (props: FiltersProps) => { +export interface FiltersProps extends AbstractFiltersProps { + pref: AppDetailsPreferences; + tree: ApplicationTree; +} + +export interface AppSetFiltersProps extends AbstractFiltersProps { + pref: AppSetDetailsPreferences; +} + +export const Filters = (props: AbstractFiltersProps) => { const ctx = React.useContext(Context); const {pref, tree, onSetFilter} = props; @@ -152,7 +161,7 @@ export const Filters = (props: FiltersProps) => { })) })} {namespaces.length > 1 && ResourceFilter({label: 'NAMESPACES', prefix: 'namespace', options: (namespaces || []).filter(l => l && l !== '').map(toOption), field: true})} - {(tree.orphanedNodes || []).length > 0 && ( + {((isInvokedFromAppsPath(ctx.history.location.pathname) ? (tree as ApplicationTree).orphanedNodes : []) || []).length > 0 && (
{ public render() { return ( - services.viewPreferences.getPreferences()}> + services.viewPreferences.getPreferences() as Observable}> {prefs => { const podPrefs = prefs.appDetails.podView || ({} as PodViewPreferences); const groups = this.processTree(podPrefs.sortMode, this.props.tree.hosts || []) || []; @@ -75,7 +76,10 @@ export class PodView extends React.Component { style={{border: 'none', width: '170px'}} onClick={() => services.viewPreferences.updatePreferences({ - appDetails: {...prefs.appDetails, podView: {...podPrefs, hideUnschedulable: !podPrefs.hideUnschedulable}} + appDetails: { + ...prefs.appDetails, + podView: {...podPrefs, hideUnschedulable: !podPrefs.hideUnschedulable} + } as AppDetailsPreferences }) }> @@ -270,7 +274,7 @@ export class PodView extends React.Component { ), action: () => { this.appContext.apis.navigation.goto('.', {podSortMode: mode}); - services.viewPreferences.updatePreferences({appDetails: {...prefs.appDetails, podView: {...podPrefs, sortMode: mode}}}); + services.viewPreferences.updatePreferences({appDetails: {...prefs.appDetails, podView: {...podPrefs, sortMode: mode}} as AppDetailsPreferences}); } })); } diff --git a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx index 3d5b1782a0e0c..031d398ece5e5 100644 --- a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx +++ b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx @@ -19,6 +19,7 @@ import { getAppOverridesCount, HealthStatusIcon, isAppNode, + isApp, isYoungerThanXMinutes, NodeId, nodeKey, @@ -29,6 +30,7 @@ import {NodeUpdateAnimation} from './node-update-animation'; import {PodGroup} from '../application-pod-view/pod-view'; import './application-resource-tree.scss'; import {ArrowConnector} from './arrow-connector'; +import {Application, ApplicationSet, ApplicationTree} from '../../../shared/models'; function treeNodeKey(node: NodeId & {uid?: string}) { return node.uid || nodeKey(node); @@ -47,9 +49,9 @@ export interface ResourceTreeNode extends models.ResourceNode { isExpanded?: boolean; } -export interface ApplicationResourceTreeProps { - app: models.Application; - tree: models.ApplicationTree; +export interface AbstractApplicationResourceTreeProps { + app: models.AbstractApplication; + tree: models.AbstractApplicationTree; useNetworkingHierarchy: boolean; nodeFilter: (node: ResourceTreeNode) => boolean; selectedNodeFullName?: string; @@ -64,7 +66,6 @@ export interface ApplicationResourceTreeProps { updateUsrHelpTipMsgs: (userMsgs: models.UserMessages) => void; setShowCompactNodes: (showCompactNodes: boolean) => void; zoom: number; - podGroupCount: number; filters?: string[]; setTreeFilterGraph?: (filterGraph: any[]) => void; nameDirection: boolean; @@ -72,6 +73,15 @@ export interface ApplicationResourceTreeProps { getNodeExpansion: (node: string) => boolean; } +export interface ApplicationResourceTreeProps extends AbstractApplicationResourceTreeProps { + podGroupCount: number; +} + +export interface ApplicationSetResourceTreeProps extends AbstractApplicationResourceTreeProps { + // FFU + dummyMemberToPlacateLinter: any; +} + interface Line { x1: number; y1: number; @@ -249,7 +259,7 @@ export function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) ); } -function appNodeKey(app: models.Application) { +function appNodeKey(app: models.AbstractApplication) { return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace}); } @@ -412,7 +422,7 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R } const appNode = isAppNode(node); const rootNode = !node.root; - const extLinks: string[] = props.app.status.summary.externalURLs; + const extLinks: string[] = isApp(props.app) ? (props.app as models.Application).status.summary.externalURLs : []; const podGroupChildren = childMap.get(treeNodeKey(node)); const nonPodChildren = podGroupChildren?.reduce((acc, child) => { if (child.kind !== 'Pod') { @@ -753,7 +763,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod } const appNode = isAppNode(node); const rootNode = !node.root; - const extLinks: string[] = props.app.status.summary.externalURLs; + const extLinks: string[] = isApp(props.app) ? (props.app as models.Application).status.summary.externalURLs : []; const childCount = nodesHavingChildren.get(node.uid); return (
{ +export const ApplicationResourceTree = (props: AbstractApplicationResourceTreeProps) => { const graph = new dagre.graphlib.Graph(); graph.setGraph({nodesep: 25, rankdir: 'LR', marginy: 45, marginx: -100, ranksep: 80}); graph.setDefaultEdgeLabel(() => ({})); @@ -893,8 +903,8 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => group: 'argoproj.io', version: '', children: Array(), - status: props.app.status.sync.status, - health: props.app.status.health, + status: isApp(props.app) ? props.app.status.sync.status : null, + health: isApp(props.app) ? props.app.status.health : props.app.status, uid: props.app.kind + '-' + props.app.metadata.namespace + '-' + props.app.metadata.name, info: overridesCount > 0 @@ -908,11 +918,17 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => }; const statusByKey = new Map(); - props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res)); + if (isApp(props.app)) { + (props.app as models.Application).status.resources.forEach(res => statusByKey.set(nodeKey(res), res)); + } else { + // Assuming AppSet. Revisit this if a third type of an AbstractApp is born. + (props.app as models.ApplicationSet).status.resources.forEach(res => statusByKey.set(nodeKey(res), res)); + } + const nodeByKey = new Map(); props.tree.nodes .map(node => ({...node, orphaned: false})) - .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true}))) + .concat(((props.showOrphanedResources && isApp(props.app) ? (props.tree as ApplicationTree).orphanedNodes : []) || []).map(node => ({...node, orphaned: true}))) .forEach(node => { const status = statusByKey.get(nodeKey(node)); const resourceNode: ResourceTreeNode = {...node}; @@ -940,18 +956,20 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => props.setTreeFilterGraph(filteredGraph); } }, [props.filters]); - const {podGroupCount, userMsgs, updateUsrHelpTipMsgs, setShowCompactNodes} = props; - const podCount = nodes.filter(node => node.kind === 'Pod').length; - React.useEffect(() => { - if (podCount > podGroupCount) { - const userMsg = getUsrMsgKeyToDisplay(appNode.name, 'groupNodes', userMsgs); - updateUsrHelpTipMsgs(userMsg); - if (!userMsg.display) { - setShowCompactNodes(true); + if (isApp(props.app)) { + const podCount = nodes.filter(node => node.kind === 'Pod').length; + const {podGroupCount, userMsgs, updateUsrHelpTipMsgs, setShowCompactNodes} = props as ApplicationResourceTreeProps; + React.useEffect(() => { + if (podCount > podGroupCount) { + const userMsg = getUsrMsgKeyToDisplay(appNode.name, 'groupNodes', userMsgs); + updateUsrHelpTipMsgs(userMsg); + if (!userMsg.display) { + setShowCompactNodes(true); + } } - } - }, [podCount]); + }, [podCount]); + } function filterGraph(app: models.Application, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) { const appKey = appNodeKey(app); @@ -978,7 +996,7 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => } } - if (props.useNetworkingHierarchy) { + if (props.useNetworkingHierarchy && isApp(props.app)) { // Network view const hasParents = new Set(); const networkNodes = nodes.filter(node => node.networkingInfo); @@ -1002,7 +1020,7 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => hiddenNodes.push(child); } } else { - processPodGroup(parent, child, props); + processPodGroup(parent, child, props as ApplicationResourceTreeProps); } }); }); @@ -1076,8 +1094,10 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => } } else { // Tree view - const managedKeys = new Set(props.app.status.resources.map(nodeKey)); - const orphanedKeys = new Set(props.tree.orphanedNodes?.map(nodeKey)); + const managedKeys = isApp(props.app) + ? new Set((props.app as Application).status.resources.map(nodeKey)) + : new Set((props.app as ApplicationSet).status.resources.map(nodeKey)); + const orphanedKeys = isApp(props.app) ? new Set((props.tree as ApplicationTree).orphanedNodes?.map(nodeKey)) : new Set(); const orphans: ResourceTreeNode[] = []; let allChildNodes: ResourceTreeNode[] = []; nodesHavingChildren.set(appNode.uid, 1); @@ -1106,7 +1126,9 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => } } else { const parentTreeNode = nodeByKey.get(parentId); - processPodGroup(parentTreeNode, node, props); + if (isApp(props.app)) { + processPodGroup(parentTreeNode, node, props as ApplicationResourceTreeProps); + } } if (props.showCompactNodes) { if (childrenMap.has(parentId)) { @@ -1247,11 +1269,19 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => case NODE_TYPES.externalLoadBalancer: return {renderLoadBalancerNode(node as any)}; case NODE_TYPES.groupedNodes: - return {renderGroupedNodes(props, node as any)}; + return {renderGroupedNodes(props as ApplicationResourceTreeProps, node as any)}; case NODE_TYPES.podGroup: - return {renderPodGroup(props, key, node as ResourceTreeNode & dagre.Node, childrenMap)}; + return ( + + {renderPodGroup(props as ApplicationResourceTreeProps, key, node as ResourceTreeNode & dagre.Node, childrenMap)} + + ); default: - return {renderResourceNode(props, key, node as ResourceTreeNode & dagre.Node, nodesHavingChildren)}; + return ( + + {renderResourceNode(props as ApplicationResourceTreeProps, key, node as ResourceTreeNode & dagre.Node, nodesHavingChildren)} + + ); } })} {edges.map(edge => ( diff --git a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx index 7c2b65cd3ce27..1eedfb1fb7027 100644 --- a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx +++ b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx @@ -5,14 +5,14 @@ import {Revision} from '../../../shared/components/revision'; import {Timestamp} from '../../../shared/components/timestamp'; import * as models from '../../../shared/models'; import {services} from '../../../shared/services'; -import {ApplicationSyncWindowStatusIcon, ComparisonStatusIcon, getAppDefaultSource, getAppOperationState} from '../utils'; +import {AppSetHealthStatusIcon, ApplicationSyncWindowStatusIcon, ComparisonStatusIcon, getAppDefaultSource, getAppOperationState, getAppSetHealthStatus, isApp} from '../utils'; import {getConditionCategory, HealthStatusIcon, OperationState, syncStatusMessage, helpTip} from '../utils'; import {RevisionMetadataPanel} from './revision-metadata-panel'; import './application-status-panel.scss'; interface Props { - application: models.Application; + application: models.AbstractApplication; showDiff?: () => any; showOperation?: () => any; showConditions?: () => any; @@ -47,21 +47,35 @@ const sectionHeader = (info: SectionInfo, hasMultipleSources: boolean, onClick?: }; export const ApplicationStatusPanel = ({application, showDiff, showOperation, showConditions, showExtension, showMetadataInfo}: Props) => { - const today = new Date(); - + let cntByCategory; + let hasMultipleSources; + let source; + let appOperationState: models.OperationState; let daysSinceLastSynchronized = 0; - const history = application.status.history || []; - if (history.length > 0) { - const deployDate = new Date(history[history.length - 1].deployedAt); - daysSinceLastSynchronized = Math.round(Math.abs((today.getTime() - deployDate.getTime()) / (24 * 60 * 60 * 1000))); - } - const cntByCategory = (application.status.conditions || []).reduce( - (map, next) => map.set(getConditionCategory(next), (map.get(getConditionCategory(next)) || 0) + 1), - new Map() - ); - const appOperationState = getAppOperationState(application); - if (application.metadata.deletionTimestamp && !appOperationState) { - showOperation = null; + + if (isApp) { + const today = new Date(); + + const history = application.status.history || []; + if (history.length > 0) { + const deployDate = new Date(history[history.length - 1].deployedAt); + daysSinceLastSynchronized = Math.round(Math.abs((today.getTime() - deployDate.getTime()) / (24 * 60 * 60 * 1000))); + } + cntByCategory = ((application as models.Application).status.conditions || []).reduce( + (map, next) => map.set(getConditionCategory(next), (map.get(getConditionCategory(next)) || 0) + 1), + new Map() + ); + appOperationState = getAppOperationState(application); + if (application.metadata.deletionTimestamp && !appOperationState) { + showOperation = null; + } + hasMultipleSources = (application as models.Application).spec.sources && (application as models.Application).spec.sources.length > 0; + source = getAppDefaultSource(application as models.Application); + } else { + cntByCategory = ((application as models.ApplicationSet).status.conditions || []).reduce( + (map, next) => map.set(getConditionCategory(next), (map.get(getConditionCategory(next)) || 0) + 1), + new Map() + ); } const statusExtensions = services.extensions.getStatusPanelExtensions(); @@ -69,57 +83,76 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh const infos = cntByCategory.get('info'); const warnings = cntByCategory.get('warning'); const errors = cntByCategory.get('error'); - const source = getAppDefaultSource(application); - const hasMultipleSources = application.spec.sources && application.spec.sources.length > 0; + return (
-
{sectionLabel({title: 'APP HEALTH', helpContent: 'The health status of your app'})}
+
+ {sectionLabel( + isApp(application) + ? {title: 'APP HEALTH', helpContent: 'The health status of your app'} + : {title: 'APPSET HEALTH', helpContent: 'The health status of your appset'} + )} +
- + {isApp(application) && } + {!isApp(application) && }   - {application.status.health.status} + {isApp(application) ? (application as models.Application).status.health.status : getAppSetHealthStatus((application as models.ApplicationSet).status)}
- {application.status.health.message &&
{application.status.health.message}
} + {isApp(application) && (application as models.Application).status.health.message && ( +
{(application as models.Application).status.health.message}
+ )} + {!isApp(application) && (application as models.ApplicationSet).status.conditions[0].message && ( +
{(application as models.ApplicationSet).status.conditions[0].message}
+ )}
-
- - {sectionHeader( - { - title: 'SYNC STATUS', - helpContent: 'Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.' - }, - hasMultipleSources, - () => showMetadataInfo(application.status.sync ? application.status.sync.revision : '') - )} -
-
- {application.status.sync.status === models.SyncStatuses.OutOfSync ? ( - showDiff && showDiff()}> - - - ) : ( - - )} + {isApp(application) && ( +
+ + {sectionHeader( + { + title: 'SYNC STATUS', + helpContent: 'Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.' + }, + hasMultipleSources, + () => showMetadataInfo((application as models.Application).status.sync ? (application as models.Application).status.sync.revision : '') + )} +
+
+ {(application as models.Application).status.sync.status === models.SyncStatuses.OutOfSync ? ( + showDiff && showDiff()}> + + + ) : ( + + )} +
+
{syncStatusMessage(application)}
-
{syncStatusMessage(application)}
-
-
- {application.spec.syncPolicy?.automated ? 'Auto sync is enabled.' : 'Auto sync is not enabled.'} -
- {application.status && application.status.sync && application.status.sync.revision && !application.spec.source.chart && ( -
- +
+ {(application as models.Application).spec.syncPolicy?.automated ? 'Auto sync is enabled.' : 'Auto sync is not enabled.'}
- )} - -
- {appOperationState && ( + {(application as models.Application).status && + (application as models.Application).status.sync && + (application as models.Application).status.sync.revision && + !(application as models.Application).spec.source.chart && ( +
+ +
+ )} + +
+ )} + {isApp(application) && appOperationState && (
{sectionHeader( @@ -160,7 +193,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh )} {application.status.conditions && (
- {sectionLabel({title: 'APP CONDITIONS'})} + {sectionLabel(isApp(application) ? {title: 'APP CONDITIONS'} : {title: 'APPSET CONDITIONS'})}
)} - { - return await services.applications.getApplicationSyncWindowState(app.metadata.name, app.metadata.namespace); - }}> - {(data: models.ApplicationSyncWindowState) => ( - - {data.assignedWindows && ( -
- {sectionLabel({ - title: 'SYNC WINDOWS', - helpContent: - 'The aggregate state of sync windows for this app. ' + - 'Red: no syncs allowed. ' + - 'Yellow: manual syncs allowed. ' + - 'Green: all syncs allowed' - })} -
- + {isApp(application) && ( + { + return await services.applications.getApplicationSyncWindowState(app.metadata.name, app.metadata.namespace); + }}> + {(data: models.ApplicationSyncWindowState) => ( + + {data.assignedWindows && ( +
+ {sectionLabel({ + title: 'SYNC WINDOWS', + helpContent: + 'The aggregate state of sync windows for this app. ' + + 'Red: no syncs allowed. ' + + 'Yellow: manual syncs allowed. ' + + 'Green: all syncs allowed' + })} +
+ +
-
- )} - - )} - + )} + + )} + + )} {statusExtensions && statusExtensions.map(ext => showExtension && showExtension(ext.id)} />)}
); diff --git a/ui/src/app/applications/components/applications-container.tsx b/ui/src/app/applications/components/applications-container.tsx index 756f7ea22f2d8..e8e86a474a73d 100644 --- a/ui/src/app/applications/components/applications-container.tsx +++ b/ui/src/app/applications/components/applications-container.tsx @@ -3,13 +3,16 @@ import {Route, RouteComponentProps, Switch} from 'react-router'; import {ApplicationDetails} from './application-details/application-details'; import {ApplicationFullscreenLogs} from './application-fullscreen-logs/application-fullscreen-logs'; import {ApplicationsList} from './applications-list/applications-list'; +// import {ApplicationSetsList, ApplicationsList} from './applications-list/applications-list'; export const ApplicationsContainer = (props: RouteComponentProps) => ( - + } /> + + {/* */} ); diff --git a/ui/src/app/applications/components/applications-list/applications-filter.tsx b/ui/src/app/applications/components/applications-list/applications-filter.tsx index af1da7a371d0f..f88eb5a4ffa17 100644 --- a/ui/src/app/applications/components/applications-list/applications-filter.tsx +++ b/ui/src/app/applications/components/applications-list/applications-filter.tsx @@ -2,27 +2,69 @@ import {useData, Checkbox} from 'argo-ui/v2'; import * as minimatch from 'minimatch'; import * as React from 'react'; import {Context} from '../../../shared/context'; -import {Application, ApplicationDestination, Cluster, HealthStatusCode, HealthStatuses, SyncPolicy, SyncStatusCode, SyncStatuses} from '../../../shared/models'; -import {AppsListPreferences, services} from '../../../shared/services'; +import { + AbstractApplication, + Application, + ApplicationDestination, + ApplicationSet, + ApplicationSetConditionStatuses, + ApplicationSetSpec, + ApplicationSetStatus, + ApplicationSpec, + ApplicationStatus, + Cluster, + HealthStatusCode, + HealthStatuses, + Operation, + SyncPolicy, + SyncStatusCode, + SyncStatuses +} from '../../../shared/models'; +import {AbstractAppsListPreferences, AppSetsListPreferences, AppsListPreferences, services} from '../../../shared/services'; import {Filter, FiltersGroup} from '../filter/filter'; import * as LabelSelector from '../label-selector'; -import {ComparisonStatusIcon, getAppDefaultSource, HealthStatusIcon} from '../utils'; +import {ComparisonStatusIcon, getAppDefaultSource, getAppSetHealthStatus, HealthStatusIcon, isApp, isInvokedFromAppsPath} from '../utils'; +import {ContextApis} from '../../../shared/context'; +import {History} from 'history'; -export interface FilterResult { +export interface AbstractFilterResult { + favourite: boolean; + labels: boolean; + health: boolean; +} + +export interface FilterResult extends AbstractFilterResult { repos: boolean; sync: boolean; autosync: boolean; - health: boolean; - namespaces: boolean; clusters: boolean; - favourite: boolean; - labels: boolean; } -export interface FilteredApp extends Application { +export interface ApplicationSetFilterResult extends AbstractFilterResult { + // FFU + dummyToPlacateLinter: any; +} + +export interface AbstractFilteredApp extends AbstractApplication { + filterResult: AbstractFilterResult; +} + +export interface FilteredApp extends AbstractFilteredApp { + spec: ApplicationSpec; + status: ApplicationStatus; + operation?: Operation; + isAppOfAppsPattern?: boolean; + filterResult: FilterResult; } +export interface ApplicationSetFilteredApp extends AbstractFilteredApp { + spec: ApplicationSetSpec; + status: ApplicationSetStatus; + + filterResult: ApplicationSetFilterResult; +} + function getAutoSyncStatus(syncPolicy?: SyncPolicy) { if (!syncPolicy || !syncPolicy.automated) { return 'Disabled'; @@ -30,30 +72,42 @@ function getAutoSyncStatus(syncPolicy?: SyncPolicy) { return 'Enabled'; } -export function getFilterResults(applications: Application[], pref: AppsListPreferences): FilteredApp[] { +export function getFilterResults(applications: AbstractApplication[], pref: AbstractAppsListPreferences): AbstractFilteredApp[] { return applications.map(app => ({ ...app, - filterResult: { - repos: pref.reposFilter.length === 0 || pref.reposFilter.includes(getAppDefaultSource(app).repoURL), - sync: pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status), - autosync: pref.autoSyncFilter.length === 0 || pref.autoSyncFilter.includes(getAutoSyncStatus(app.spec.syncPolicy)), - health: pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status), - namespaces: pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns)), - favourite: !pref.showFavorites || (pref.favoritesAppList && pref.favoritesAppList.includes(app.metadata.name)), - clusters: - pref.clustersFilter.length === 0 || - pref.clustersFilter.some(filterString => { - const match = filterString.match('^(.*) [(](http.*)[)]$'); - if (match?.length === 3) { - const [, name, url] = match; - return url === app.spec.destination.server || name === app.spec.destination.name; - } else { - const inputMatch = filterString.match('^http.*$'); - return (inputMatch && inputMatch[0] === app.spec.destination.server) || (app.spec.destination.name && minimatch(app.spec.destination.name, filterString)); - } - }), - labels: pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels)) - } + filterResult: isApp(app) + ? { + repos: (pref as AppsListPreferences).reposFilter.length === 0 || (pref as AppsListPreferences).reposFilter.includes(getAppDefaultSource(app).repoURL), + sync: (pref as AppsListPreferences).syncFilter.length === 0 || (pref as AppsListPreferences).syncFilter.includes(app.status.sync.status), + autosync: + (pref as AppsListPreferences).autoSyncFilter.length === 0 || + (pref as AppsListPreferences).autoSyncFilter.includes(getAutoSyncStatus((app as Application).spec.syncPolicy)), + health: pref.healthFilter.length === 0 || pref.healthFilter.includes((app as Application).status.health.status), + namespaces: + (pref as AppsListPreferences).namespacesFilter.length === 0 || + (pref as AppsListPreferences).namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns)), + favourite: !pref.showFavorites || (pref.favoritesAppList && pref.favoritesAppList.includes(app.metadata.name)), + clusters: + (pref as AppsListPreferences).clustersFilter.length === 0 || + (pref as AppsListPreferences).clustersFilter.some(filterString => { + const match = filterString.match('^(.*) [(](http.*)[)]$'); + if (match?.length === 3) { + const [, name, url] = match; + return url === app.spec.destination.server || name === app.spec.destination.name; + } else { + const inputMatch = filterString.match('^http.*$'); + return ( + (inputMatch && inputMatch[0] === app.spec.destination.server) || (app.spec.destination.name && minimatch(app.spec.destination.name, filterString)) + ); + } + }), + labels: pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels)) + } + : { + health: pref.healthFilter.length === 0 || pref.healthFilter.includes(getAppSetHealthStatus((app as ApplicationSet).status)), + favourite: !pref.showFavorites || (pref.favoritesAppList && pref.favoritesAppList.includes(app.metadata.name)), + labels: pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels)) + } })); } @@ -65,12 +119,32 @@ const optionsFrom = (options: string[], filter: string[]) => { }); }; -interface AppFilterProps { +interface AbstractAppFilterProps { + apps: AbstractFilteredApp[]; // FilteredApp[] | ApplicationSetFilteredApp[]; + pref: AbstractAppsListPreferences; + onChange: (newPrefs: AbstractAppsListPreferences) => void; + children?: React.ReactNode; + collapsed?: boolean; +} +interface AppFilterProps extends AbstractAppFilterProps { apps: FilteredApp[]; pref: AppsListPreferences; onChange: (newPrefs: AppsListPreferences) => void; - children?: React.ReactNode; - collapsed?: boolean; +} + +interface ApplicationSetFilterProps extends AbstractAppFilterProps { + apps: ApplicationSetFilteredApp[]; + pref: AppSetsListPreferences; + onChange: (newPrefs: AppSetsListPreferences) => void; +} +export function isAppFilterProps( + abstractAppFilterProps: AbstractAppFilterProps, + ctx: ContextApis & { + history: History; + } +): abstractAppFilterProps is AppFilterProps { + // return isApp(abstractAppFilterProps.apps[0]); + return isInvokedFromAppsPath(ctx.history.location.pathname); } const getCounts = (apps: FilteredApp[], filterType: keyof FilterResult, filter: (app: Application) => string, init?: string[]) => { @@ -85,6 +159,18 @@ const getCounts = (apps: FilteredApp[], filterType: keyof FilterResult, filter: return map; }; +const getAppSetCounts = (apps: ApplicationSetFilteredApp[], filterType: keyof ApplicationSetFilterResult, filter: (app: ApplicationSet) => string, init?: string[]) => { + const map = new Map(); + if (init) { + init.forEach(key => map.set(key, 0)); + } + // filter out all apps that does not match other filters and ignore this filter result + apps.filter(app => filter(app) && Object.keys(app.filterResult).every((key: keyof ApplicationSetFilterResult) => key === filterType || app.filterResult[key])).forEach(app => + map.set(filter(app), (map.get(filter(app)) || 0) + 1) + ); + return map; +}; + const getOptions = (apps: FilteredApp[], filterType: keyof FilterResult, filter: (app: Application) => string, keys: string[], getIcon?: (k: string) => React.ReactNode) => { const counts = getCounts(apps, filterType, filter, keys); return keys.map(k => { @@ -96,6 +182,23 @@ const getOptions = (apps: FilteredApp[], filterType: keyof FilterResult, filter: }); }; +const getAppSetOptions = ( + apps: ApplicationSetFilteredApp[], + filterType: keyof ApplicationSetFilterResult, + filter: (app: ApplicationSet) => string, + keys: string[], + getIcon?: (k: string) => React.ReactNode +) => { + const counts = getAppSetCounts(apps, filterType, filter, keys); + return keys.map(k => { + return { + label: k, + icon: getIcon && getIcon(k), + count: counts.get(k) + }; + }); +}; + const SyncFilter = (props: AppFilterProps) => ( ( /> ); -const HealthFilter = (props: AppFilterProps) => ( - props.onChange({...props.pref, healthFilter: s})} - options={getOptions( - props.apps, - 'health', - app => app.status.health.status, - Object.keys(HealthStatuses), - s => ( - - ) - )} - /> -); +const HealthFilter = (props: AbstractAppFilterProps) => { + const ctx = React.useContext(Context); + return ( + + isAppFilterProps(props, ctx) ? props.onChange({...props.pref, healthFilter: s}) : props.onChange({...(props as ApplicationSetFilterProps).pref, healthFilter: s}) + } + options={ + isAppFilterProps(props, ctx) + ? getOptions( + props.apps, + 'health', + app => app.status.health.status, + Object.keys(HealthStatuses), + s => + ) + : getAppSetOptions( + (props as ApplicationSetFilterProps).apps, + 'health', + app => getAppSetHealthStatus(app.status), + Object.keys(ApplicationSetConditionStatuses) + // state={(app as ApplicationSet).status + /* + export interface ApplicationSetStatus { + conditions?: ApplicationSetCondition[]; + applicationStatus: ApplicationSetApplicationStatus[]; +} + */ + // s => + ) + } + /> + ); +}; -const LabelsFilter = (props: AppFilterProps) => { +const LabelsFilter = (props: AbstractAppFilterProps) => { const labels = new Map>(); - props.apps + (props.apps as AbstractFilteredApp[]) .filter(app => app.metadata && app.metadata.labels) .forEach(app => Object.keys(app.metadata.labels).forEach(label => { @@ -224,7 +347,7 @@ const NamespaceFilter = (props: AppFilterProps) => { ); }; -const FavoriteFilter = (props: AppFilterProps) => { +const FavoriteFilter = (props: AbstractAppFilterProps) => { const ctx = React.useContext(Context); const onChange = (val: boolean) => { ctx.navigation.goto('.', {showFavorites: val}, {replace: true}); @@ -276,17 +399,18 @@ const AutoSyncFilter = (props: AppFilterProps) => ( /> ); -export const ApplicationsFilter = (props: AppFilterProps) => { +export const ApplicationsFilter = (props: AbstractAppFilterProps) => { + const ctx = React.useContext(Context); return ( - + {isAppFilterProps(props, ctx) && } - - - - + {isAppFilterProps(props, ctx) && } + {isAppFilterProps(props, ctx) && } + {isAppFilterProps(props, ctx) && } + {isAppFilterProps(props, ctx) && } ); }; diff --git a/ui/src/app/applications/components/applications-list/applications-list.scss b/ui/src/app/applications/components/applications-list/applications-list.scss index 6d359e59723e3..da7a07118ff90 100644 --- a/ui/src/app/applications/components/applications-list/applications-list.scss +++ b/ui/src/app/applications/components/applications-list/applications-list.scss @@ -43,6 +43,11 @@ border-left-color: $argo-success-color; } + // For AppSet + &--health-True { + border-left-color: $argo-success-color; + } + // intermediate statuses &--health-Progressing { border-left-color: $argo-running-color; @@ -57,6 +62,11 @@ border-left-color: $argo-failed-color; } + // For AppSet + &--health-False { + border-left-color: $argo-failed-color; + } + &--health-Unknown { border-left-color: $argo-color-gray-4; } @@ -218,4 +228,4 @@ i.menu_icon { margin: 0 auto !important; } } -} \ No newline at end of file +} diff --git a/ui/src/app/applications/components/applications-list/applications-list.tsx b/ui/src/app/applications/components/applications-list/applications-list.tsx index d6ddfeb343e66..70f7b34d623d9 100644 --- a/ui/src/app/applications/components/applications-list/applications-list.tsx +++ b/ui/src/app/applications/components/applications-list/applications-list.tsx @@ -9,21 +9,31 @@ import {bufferTime, delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/ import {AddAuthToToolbar, ClusterCtx, DataLoader, EmptyState, ObservableQuery, Page, Paginate, Query, Spinner} from '../../../shared/components'; import {AuthSettingsCtx, Consumer, Context, ContextApis} from '../../../shared/context'; import * as models from '../../../shared/models'; -import {AppsListViewKey, AppsListPreferences, AppsListViewType, HealthStatusBarPreferences, services} from '../../../shared/services'; +import { + AppsListViewKey, + AppsListPreferences, + AppsListViewType, + HealthStatusBarPreferences, + services, + AbstractAppsListPreferences, + AppSetsListPreferences +} from '../../../shared/services'; import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel'; import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel'; import {ApplicationsSyncPanel} from '../applications-sync-panel/applications-sync-panel'; import * as AppUtils from '../utils'; -import {ApplicationsFilter, FilteredApp, getFilterResults} from './applications-filter'; +import {AbstractFilteredApp, ApplicationsFilter, getFilterResults} from './applications-filter'; import {ApplicationsStatusBar} from './applications-status-bar'; import {ApplicationsSummary} from './applications-summary'; import {ApplicationsTable} from './applications-table'; -import {ApplicationTiles} from './applications-tiles'; +import {AbstractApplicationTilesProps, ApplicationSetTilesProps, ApplicationTiles, ApplicationTilesProps} from './applications-tiles'; import {ApplicationsRefreshPanel} from '../applications-refresh-panel/applications-refresh-panel'; import {useSidebarTarget} from '../../../sidebar/sidebar'; import './applications-list.scss'; import './flex-top-bar.scss'; +import {AbstractApplication, Application, ApplicationSet} from '../../../shared/models'; +import {History} from 'history'; const EVENTS_BUFFER_TIMEOUT = 500; const WATCH_RETRY_TIMEOUT = 500; @@ -48,17 +58,39 @@ const APP_FIELDS = [ 'status.summary', 'status.resources' ]; -const APP_LIST_FIELDS = ['metadata.resourceVersion', ...APP_FIELDS.map(field => `items.${field}`)]; -const APP_WATCH_FIELDS = ['result.type', ...APP_FIELDS.map(field => `result.application.${field}`)]; -function loadApplications(projects: string[], appNamespace: string): Observable { - return from(services.applications.list(projects, {appNamespace, fields: APP_LIST_FIELDS})).pipe( +const APPSET_FIELDS = ['metadata.name', 'metadata.namespace', 'metadata.annotations', 'metadata.labels', 'metadata.creationTimestamp', 'metadata.deletionTimestamp', 'spec']; + +function getAppListFields(isFromApps: boolean): string[] { + const APP_LIST_FIELDS = isFromApps + ? ['metadata.resourceVersion', ...APP_FIELDS.map(field => `items.${field}`)] + : ['metadata.resourceVersion', ...APPSET_FIELDS.map(field => `items.${field}`)]; + return APP_LIST_FIELDS; +} + +function getAppWatchFields(isFromApps: boolean): string[] { + const APP_WATCH_FIELDS = isFromApps + ? ['result.type', ...APP_FIELDS.map(field => `result.application.${field}`)] + : ['result.type', ...APPSET_FIELDS.map(field => `result.application.${field}`)]; + return APP_WATCH_FIELDS; +} + +function loadApplications( + ctx: ContextApis & { + history: History; + }, + projects: string[], + appNamespace: string, + objectListKind: string +): Observable { + const isListOfApplications = objectListKind === 'application'; + return from(services.applications.list(projects, ctx, {appNamespace, fields: getAppListFields(isListOfApplications)})).pipe( mergeMap(applicationsList => { const applications = applicationsList.items; return merge( from([applications]), services.applications - .watch({projects, resourceVersion: applicationsList.metadata.resourceVersion}, {fields: APP_WATCH_FIELDS}) + .watch(ctx.history.location.pathname, {projects, resourceVersion: applicationsList.metadata.resourceVersion}, {fields: getAppWatchFields(isListOfApplications)}) .pipe(repeat()) .pipe(retryWhen(errors => errors.pipe(delay(WATCH_RETRY_TIMEOUT)))) // batch events to avoid constant re-rendering and improve UI performance @@ -67,6 +99,7 @@ function loadApplications(projects: string[], appNamespace: string): Observable< map(appChanges => { appChanges.forEach(appChange => { const index = applications.findIndex(item => AppUtils.appInstanceName(item) === AppUtils.appInstanceName(appChange.application)); + switch (appChange.type) { case 'DELETED': if (index > -1) { @@ -87,91 +120,115 @@ function loadApplications(projects: string[], appNamespace: string): Observable< ) .pipe(filter(item => item.updated)) .pipe(map(item => item.applications)) + // .pipe(map(item => (isApp(applications[0]) ? (item.applications as models.Application[]) : (item.applications as models.ApplicationSet[])))) ); }) ); } -const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: number; search: string}) => React.ReactNode}) => ( - - {q => ( - - combineLatest([services.viewPreferences.getPreferences().pipe(map(item => item.appList)), q]).pipe( - map(items => { - const params = items[1]; - const viewPref: AppsListPreferences = {...items[0]}; - if (params.get('proj') != null) { - viewPref.projectsFilter = params - .get('proj') - .split(',') - .filter(item => !!item); - } - if (params.get('sync') != null) { - viewPref.syncFilter = params - .get('sync') - .split(',') - .filter(item => !!item); - } - if (params.get('autoSync') != null) { - viewPref.autoSyncFilter = params - .get('autoSync') - .split(',') - .filter(item => !!item); - } - if (params.get('health') != null) { - viewPref.healthFilter = params - .get('health') - .split(',') - .filter(item => !!item); - } - if (params.get('namespace') != null) { - viewPref.namespacesFilter = params - .get('namespace') - .split(',') - .filter(item => !!item); - } - if (params.get('cluster') != null) { - viewPref.clustersFilter = params - .get('cluster') - .split(',') - .filter(item => !!item); - } - if (params.get('showFavorites') != null) { - viewPref.showFavorites = params.get('showFavorites') === 'true'; - } - if (params.get('view') != null) { - viewPref.view = params.get('view') as AppsListViewType; - } - if (params.get('labels') != null) { - viewPref.labelsFilter = params - .get('labels') - .split(',') - .map(decodeURIComponent) - .filter(item => !!item); - } - return {...viewPref, page: parseInt(params.get('page') || '0', 10), search: params.get('search') || ''}; - }) - ) - }> - {pref => children(pref)} - - )} - -); - -function filterApps(applications: models.Application[], pref: AppsListPreferences, search: string): {filteredApps: models.Application[]; filterResults: FilteredApp[]} { +const ViewPref = ({children, objectListKind}: {children: (pref: AbstractAppsListPreferences & {page: number; search: string}) => React.ReactNode; objectListKind: string}) => { + return ( + + {q => ( + + combineLatest([services.viewPreferences.getPreferences().pipe(map(item => item.appList)), q]).pipe( + map(items => { + const params = items[1]; + const viewPref: AbstractAppsListPreferences = {...items[0]}; + if (objectListKind === 'application') { + // App specific filters + if (params.get('proj') != null) { + (viewPref as AppsListPreferences).projectsFilter = params + .get('proj') + .split(',') + .filter(item => !!item); + } + if (params.get('sync') != null) { + (viewPref as AppsListPreferences).syncFilter = params + .get('sync') + .split(',') + .filter(item => !!item); + } + if (params.get('autoSync') != null) { + (viewPref as AppsListPreferences).autoSyncFilter = params + .get('autoSync') + .split(',') + .filter(item => !!item); + } + if (params.get('cluster') != null) { + (viewPref as AppsListPreferences).clustersFilter = params + .get('cluster') + .split(',') + .filter(item => !!item); + } + if (params.get('namespace') != null) { + (viewPref as AppsListPreferences).namespacesFilter = params + .get('namespace') + .split(',') + .filter(item => !!item); + } + } + // App and AppSet common filters + if (params.get('health') != null) { + viewPref.healthFilter = params + .get('health') + .split(',') + .filter(item => !!item); + } + + if (params.get('showFavorites') != null) { + viewPref.showFavorites = params.get('showFavorites') === 'true'; + } + if (params.get('view') != null) { + viewPref.view = params.get('view') as AppsListViewType; + } + if (params.get('labels') != null) { + viewPref.labelsFilter = params + .get('labels') + .split(',') + .map(decodeURIComponent) + .filter(item => !!item); + } + return {...viewPref, page: parseInt(params.get('page') || '0', 10), search: params.get('search') || ''}; + }) + ) + }> + {pref => children(pref)} + + )} + + ); +}; + +function filterApps( + applications: AbstractApplication[], + pref: AbstractAppsListPreferences, + search: string, + isListOfApplications: boolean +): {filteredApps: AbstractApplication[]; filterResults: AbstractFilteredApp[]} { applications = applications.map(app => { let isAppOfAppsPattern = false; - for (const resource of app.status.resources) { - if (resource.kind === 'Application') { - isAppOfAppsPattern = true; - break; + if (!isListOfApplications) { + // AppSet behaves like an app of apps + isAppOfAppsPattern = true; + } else { + // It is an App and may or may not be app-of-apps pattern + for (const resource of (app as models.Application).status.resources) { + if (resource.kind === 'Application') { + isAppOfAppsPattern = true; + break; + } } } return {...app, isAppOfAppsPattern}; }); - const filterResults = getFilterResults(applications, pref); + const filterResults = + applications.length === 0 + ? getFilterResults(applications, pref) + : isListOfApplications + ? getFilterResults(applications as Application[], pref as AppsListPreferences) + : getFilterResults(applications as ApplicationSet[], pref as AppSetsListPreferences); return { filterResults, filteredApps: filterResults.filter( @@ -188,7 +245,14 @@ function tryJsonParse(input: string) { } } -const SearchBar = (props: {content: string; ctx: ContextApis; apps: models.Application[]}) => { +const SearchBar = (props: { + content: string; + ctx: ContextApis & { + history: History; + }; + apps: models.AbstractApplication[]; + objectListKind: string; +}) => { const {content, ctx, apps} = {...props}; const searchBar = React.useRef(null); @@ -200,6 +264,8 @@ const SearchBar = (props: {content: string; ctx: ContextApis; apps: models.Appli const [isFocused, setFocus] = React.useState(false); const useAuthSettingsCtx = React.useContext(AuthSettingsCtx); + const placeholderText = props.objectListKind === 'application' ? 'Search applications...' : 'Search application sets...'; + useKeybinding({ keys: Key.SLASH, action: () => { @@ -248,7 +314,7 @@ const SearchBar = (props: {content: string; ctx: ContextApis; apps: models.Appli }} style={{fontSize: '14px'}} className='argo-field' - placeholder='Search applications...' + placeholder={placeholderText} />
/
{content && ( @@ -309,7 +375,11 @@ const FlexTopBar = (props: {toolbar: Toolbar | Observable}) => { ); }; -export const ApplicationsList = (props: RouteComponentProps<{}>) => { +interface RouteComponentPropsExtended extends RouteComponentProps { + objectListKind: string; +} + +export const ApplicationsList = (props: RouteComponentPropsExtended) => { const query = new URLSearchParams(props.location.search); const appInput = tryJsonParse(query.get('new')); const syncAppsInput = tryJsonParse(query.get('syncApps')); @@ -320,6 +390,11 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => { const loaderRef = React.useRef(); const {List, Summary, Tiles} = AppsListViewKey; + const listCtx = React.useContext(Context); + + const objectListKind = props.objectListKind; + const isListOfApplications = objectListKind === 'application'; + function refreshApp(appName: string, appNamespace: string) { // app refreshing might be done too quickly so that UI might miss it due to event batching // add refreshing annotation in the UI to improve user experience @@ -334,69 +409,127 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => { services.applications.get(appName, appNamespace, 'normal'); } - function onFilterPrefChanged(ctx: ContextApis, newPref: AppsListPreferences) { + function onFilterPrefChanged(newPref: AbstractAppsListPreferences) { services.viewPreferences.updatePreferences({appList: newPref}); - ctx.navigation.goto( + listCtx.navigation.goto( '.', + { - proj: newPref.projectsFilter.join(','), - sync: newPref.syncFilter.join(','), - autoSync: newPref.autoSyncFilter.join(','), + proj: isListOfApplications ? (newPref as AppsListPreferences).projectsFilter.join(',') : '', + sync: isListOfApplications ? (newPref as AppsListPreferences).syncFilter.join(',') : '', + autoSync: isListOfApplications ? (newPref as AppsListPreferences).autoSyncFilter.join(',') : '', health: newPref.healthFilter.join(','), - namespace: newPref.namespacesFilter.join(','), - cluster: newPref.clustersFilter.join(','), + namespace: isListOfApplications ? (newPref as AppsListPreferences).namespacesFilter.join(',') : '', + cluster: isListOfApplications ? (newPref as AppsListPreferences).clustersFilter.join(',') : '', labels: newPref.labelsFilter.map(encodeURIComponent).join(',') }, {replace: true} ); } + const pageTitlePrefix = isListOfApplications ? 'Applications ' : 'ApplicationSets '; + function getPageTitle(view: string) { switch (view) { case List: - return 'Applications List'; + return pageTitlePrefix + 'List'; case Tiles: - return 'Applications Tiles'; + return pageTitlePrefix + 'Tiles'; case Summary: - return 'Applications Summary'; + return pageTitlePrefix + 'Summary'; } return ''; } const sidebarTarget = useSidebarTarget(); + const getEmptyStateText = isListOfApplications ? 'No matching applications found' : 'No matching application sets found'; + + const applicationTilesProps = (data: models.Application[]): ApplicationTilesProps => { + return { + applications: data, + syncApplication: (appName, appNamespace) => listCtx.navigation.goto('.', {syncApp: appName, appNamespace}, {replace: true}), + + // refreshApplication: refreshApp, + deleteApplication: (appName, appNamespace) => AppUtils.deleteApplication(appName, appNamespace, listCtx), + objectListKind: '{objectListKind}' + }; + }; + + const applicationSetTilesProps = (data: models.ApplicationSet[]): ApplicationSetTilesProps => { + return { + applications: data, + deleteApplication: (appName, appNamespace) => AppUtils.deleteApplication(appName, appNamespace, listCtx), + objectListKind: '{objectListKind}' + }; + }; + + const abstractApplicationTilesProps = (applications: models.AbstractApplication[]): AbstractApplicationTilesProps => { + if (isListOfApplications) { + return applicationTilesProps(applications); + } else { + return applicationSetTilesProps(applications); + } + }; + + function getProjectsFilter(pref: AbstractAppsListPreferences): string[] { + return isListOfApplications ? (pref as AppsListPreferences & {page: number; search: string}).projectsFilter : []; + } + return ( {ctx => ( - + {pref => ( AppUtils.handlePageVisibility(() => loadApplications(pref.projectsFilter, query.get('appNamespace')))} + load={() => + AppUtils.handlePageVisibility(() => + loadApplications( + ctx, + isListOfApplications ? (pref as AppsListPreferences & {page: number; search: string}).projectsFilter : [], + query.get('appNamespace'), + objectListKind + ) + ) + } loadingRenderer={() => (
)}> - {(applications: models.Application[]) => { + {(applications: models.AbstractApplication[]) => { const healthBarPrefs = pref.statusBarView || ({} as HealthStatusBarPreferences); - const {filteredApps, filterResults} = filterApps(applications, pref, pref.search); + const {filteredApps, filterResults} = filterApps( + isListOfApplications ? (applications as Application[]) : (applications as ApplicationSet[]), + isListOfApplications + ? (pref as AppsListPreferences & {page: number; search: string}) + : (pref as AppSetsListPreferences & {page: number; search: string}), + pref.search, + isListOfApplications + ); return ( - {q => } + + {q => } +
+ {applications.length > 0 && isListOfApplications && ( + + {q => ( + + q.pipe( + mergeMap(params => { + const syncApp = params.get('syncApp'); + const appNamespace = params.get('appNamespace'); + return ( + (syncApp && + from(services.applications.get(syncApp, appNamespace, ctx.history.location.pathname))) || + from([null]) + ); + }) + ) + }> + {app => ( + ctx.navigation.goto('.', {syncApp: null}, {replace: true})} + /> + )} + + )} + + )} ctx.navigation.goto('.', {new: null}, {replace: true})} header={
- {' '} + {applications.length > 0 && isListOfApplications && ( + + )}{' '}
}> - {appInput && ( + {appInput && isListOfApplications && ( { setCreateApi(api); diff --git a/ui/src/app/applications/components/applications-list/applications-status-bar.tsx b/ui/src/app/applications/components/applications-list/applications-status-bar.tsx index c20b5612d121f..ce350a09049bd 100644 --- a/ui/src/app/applications/components/applications-list/applications-status-bar.tsx +++ b/ui/src/app/applications/components/applications-list/applications-status-bar.tsx @@ -5,44 +5,67 @@ import {Consumer} from '../../../shared/context'; import * as models from '../../../shared/models'; import './applications-status-bar.scss'; +import {getAppSetHealthStatus, isApp} from '../utils'; +import {Application, ApplicationSet} from '../../../shared/models'; export interface ApplicationsStatusBarProps { - applications: models.Application[]; + applications: models.AbstractApplication[]; } export const ApplicationsStatusBar = ({applications}: ApplicationsStatusBarProps) => { - const readings = [ - { - name: 'Healthy', - value: applications.filter(app => app.status.health.status === 'Healthy').length, - color: COLORS.health.healthy - }, - { - name: 'Progressing', - value: applications.filter(app => app.status.health.status === 'Progressing').length, - color: COLORS.health.progressing - }, - { - name: 'Degraded', - value: applications.filter(app => app.status.health.status === 'Degraded').length, - color: COLORS.health.degraded - }, - { - name: 'Suspended', - value: applications.filter(app => app.status.health.status === 'Suspended').length, - color: COLORS.health.suspended - }, - { - name: 'Missing', - value: applications.filter(app => app.status.health.status === 'Missing').length, - color: COLORS.health.missing - }, - { - name: 'Unknown', - value: applications.filter(app => app.status.health.status === 'Unknown').length, - color: COLORS.health.unknown - } - ]; + const readings: any[] = []; + if (isApp(applications[0])) { + readings.push( + { + name: 'Healthy', + value: applications.filter(app => (app as Application).status.health.status === 'Healthy').length, + color: COLORS.health.healthy + }, + { + name: 'Progressing', + value: applications.filter(app => (app as Application).status.health.status === 'Progressing').length, + color: COLORS.health.progressing + }, + { + name: 'Degraded', + value: applications.filter(app => (app as Application).status.health.status === 'Degraded').length, + color: COLORS.health.degraded + }, + { + name: 'Suspended', + value: applications.filter(app => (app as Application).status.health.status === 'Suspended').length, + color: COLORS.health.suspended + }, + { + name: 'Missing', + value: applications.filter(app => (app as Application).status.health.status === 'Missing').length, + color: COLORS.health.missing + }, + { + name: 'Unknown', + value: applications.filter(app => (app as Application).status.health.status === 'Unknown').length, + color: COLORS.health.unknown + } + ); + } else { + readings.push( + { + name: 'True', + value: applications.filter(app => getAppSetHealthStatus((app as ApplicationSet).status) === 'True').length, + color: COLORS.health.healthy + }, + { + name: 'False', + value: applications.filter(app => getAppSetHealthStatus((app as ApplicationSet).status) === 'False').length, + color: COLORS.health.degraded + }, + { + name: 'Unknown', + value: applications.filter(app => getAppSetHealthStatus((app as ApplicationSet).status) === 'Unknown').length, + color: COLORS.health.unknown + } + ); + } // will sort readings by value greatest to lowest, then by name readings.sort((a, b) => (a.value < b.value ? 1 : a.value === b.value ? (a.name > b.name ? 1 : -1) : -1)); @@ -55,7 +78,7 @@ export const ApplicationsStatusBar = ({applications}: ApplicationsStatusBarProps {ctx => ( <> - {totalItems > 1 && ( + {totalItems > 0 && (
{readings && readings.length > 1 && diff --git a/ui/src/app/applications/components/applications-list/applications-summary.tsx b/ui/src/app/applications/components/applications-list/applications-summary.tsx index 0a77350fd1127..5433cefc81bab 100644 --- a/ui/src/app/applications/components/applications-list/applications-summary.tsx +++ b/ui/src/app/applications/components/applications-list/applications-summary.tsx @@ -3,8 +3,10 @@ const PieChart = require('react-svg-piechart').default; import {COLORS} from '../../../shared/components'; import * as models from '../../../shared/models'; -import {HealthStatusCode, SyncStatusCode} from '../../../shared/models'; -import {ComparisonStatusIcon, HealthStatusIcon} from '../utils'; +import {Application, ApplicationSet, HealthStatusCode, SyncStatusCode} from '../../../shared/models'; +import {ComparisonStatusIcon, HealthStatusIcon, getAppSetHealthStatus, isInvokedFromAppsPath} from '../utils'; +import {ContextApis} from '../../../shared/context'; +import {History} from 'history'; const healthColors = new Map(); healthColors.set('Unknown', COLORS.health.unknown); @@ -14,52 +16,96 @@ healthColors.set('Healthy', COLORS.health.healthy); healthColors.set('Degraded', COLORS.health.degraded); healthColors.set('Missing', COLORS.health.missing); +const appSetHealthColors = new Map(); +appSetHealthColors.set('Unknown', COLORS.health.unknown); +appSetHealthColors.set('True', COLORS.health.healthy); +appSetHealthColors.set('False', COLORS.health.degraded); + const syncColors = new Map(); syncColors.set('Unknown', COLORS.sync.unknown); syncColors.set('Synced', COLORS.sync.synced); syncColors.set('OutOfSync', COLORS.sync.out_of_sync); -export const ApplicationsSummary = ({applications}: {applications: models.Application[]}) => { +export const ApplicationsSummary = ({ + applications, + ctx +}: { + applications: models.AbstractApplication[]; + ctx: ContextApis & { + history: History; + }; +}) => { const sync = new Map(); - applications.forEach(app => sync.set(app.status.sync.status, (sync.get(app.status.sync.status) || 0) + 1)); const health = new Map(); - applications.forEach(app => health.set(app.status.health.status, (health.get(app.status.health.status) || 0) + 1)); - const attributes = [ - { - title: 'APPLICATIONS', - value: applications.length - }, - { - title: 'SYNCED', - value: applications.filter(app => app.status.sync.status === 'Synced').length - }, - { - title: 'HEALTHY', - value: applications.filter(app => app.status.health.status === 'Healthy').length - }, - { - title: 'CLUSTERS', - value: new Set(applications.map(app => app.spec.destination.server)).size - }, - { - title: 'NAMESPACES', - value: new Set(applications.map(app => app.spec.destination.namespace)).size - } - ]; + if (isInvokedFromAppsPath(ctx.history.location.pathname)) { + applications.forEach(app => sync.set((app as Application).status.sync.status, (sync.get((app as Application).status.sync.status) || 0) + 1)); + applications.forEach(app => health.set((app as Application).status.health.status, (health.get((app as Application).status.health.status) || 0) + 1)); + } else { + applications.forEach(app => + health.set(getAppSetHealthStatus((app as ApplicationSet).status), (health.get(getAppSetHealthStatus((app as ApplicationSet).status)) || 0) + 1) + ); + } + + const attributes = isInvokedFromAppsPath(ctx.history.location.pathname) + ? [ + { + title: 'APPLICATIONS', + value: applications.length + }, + { + title: 'SYNCED', + value: applications.filter(app => app.status.sync.status === 'Synced').length + }, + { + title: 'HEALTHY', + value: applications.filter(app => app.status.health.status === 'Healthy').length + }, + { + title: 'CLUSTERS', + value: new Set(applications.map(app => app.spec.destination.server)).size + }, + { + title: 'NAMESPACES', + value: new Set(applications.map(app => app.spec.destination.namespace)).size + } + ] + : [ + { + title: 'APPLICATIONSETS', + value: applications.length + }, + { + title: 'HEALTHY', + value: applications.filter(app => getAppSetHealthStatus((app as ApplicationSet).status) === 'True').length + } + ]; + + const charts = isInvokedFromAppsPath(ctx.history.location.pathname) + ? [ + { + title: 'Sync', + data: Array.from(sync.keys()).map(key => ({title: key, value: sync.get(key), color: syncColors.get(key as models.SyncStatusCode)})), + legend: syncColors as Map + }, + { + title: 'Health', + data: Array.from(health.keys()).map(key => ({title: key, value: health.get(key), color: healthColors.get(key as models.HealthStatusCode)})), + legend: healthColors as Map + } + ] + : [ + { + title: 'Health', + data: Array.from(health.keys()).map(key => ({ + title: key, + value: health.get(key), + color: appSetHealthColors.get(key as models.ApplicationSetConditionStatus) + })), + legend: appSetHealthColors as Map + } + ]; - const charts = [ - { - title: 'Sync', - data: Array.from(sync.keys()).map(key => ({title: key, value: sync.get(key), color: syncColors.get(key as models.SyncStatusCode)})), - legend: syncColors as Map - }, - { - title: 'Health', - data: Array.from(health.keys()).map(key => ({title: key, value: health.get(key), color: healthColors.get(key as models.HealthStatusCode)})), - legend: healthColors as Map - } - ]; return (
@@ -95,7 +141,10 @@ export const ApplicationsSummary = ({applications}: {applications: models.Applic
    {Array.from(chart.legend.keys()).map(key => (
  • - {chart.title === 'Health' && } + {isInvokedFromAppsPath(ctx.history.location.pathname) && chart.title === 'Health' && ( + + )} + {/* {chart.title === 'Health' && } */} {chart.title === 'Sync' && } {` ${key} (${getLegendValue(key)})`}
  • diff --git a/ui/src/app/applications/components/applications-list/applications-table.tsx b/ui/src/app/applications/components/applications-list/applications-table.tsx index a34ea5d4d2191..7e32032be40a8 100644 --- a/ui/src/app/applications/components/applications-list/applications-table.tsx +++ b/ui/src/app/applications/components/applications-list/applications-table.tsx @@ -7,14 +7,15 @@ import {Consumer, Context} from '../../../shared/context'; import * as models from '../../../shared/models'; import {ApplicationURLs} from '../application-urls'; import * as AppUtils from '../utils'; -import {getAppDefaultSource, OperationState} from '../utils'; +import {getAppDefaultSource, getAppSetHealthStatus, isApp, OperationState} from '../utils'; import {ApplicationsLabels} from './applications-labels'; import {ApplicationsSource} from './applications-source'; import {services} from '../../../shared/services'; import './applications-table.scss'; +import {Application, ApplicationSet} from '../../../shared/models'; export const ApplicationsTable = (props: { - applications: models.Application[]; + applications: models.AbstractApplication[]; syncApplication: (appName: string, appNamespace: string) => any; refreshApplication: (appName: string, appNamespace: string) => any; deleteApplication: (appName: string, appNamespace: string) => any; @@ -37,7 +38,7 @@ export const ApplicationsTable = (props: { keys: Key.ENTER, action: () => { if (selectedApp > -1) { - ctxh.navigation.goto(`/applications/${props.applications[selectedApp].metadata.name}`); + ctxh.navigation.goto(`${AppUtils.getRootPathByPath(ctxh.history.location.pathname)}/${props.applications[selectedApp].metadata.name}`); return true; } return false; @@ -56,10 +57,18 @@ export const ApplicationsTable = (props: {
    + applications-list__entry applications-list__entry--health-${ + isApp(app) ? (app as models.Application).status.health.status : getAppSetHealthStatus((app as ApplicationSet).status) + } ${selectedApp === i ? 'applications-tiles__selected' : ''}`}>
    ctx.navigation.goto(`/applications/${app.metadata.namespace}/${app.metadata.name}`, {}, {event: e})}> + onClick={e => + ctx.navigation.goto( + `${AppUtils.getRootPathByPath(ctx.history.location.pathname)}/${app.metadata.namespace}/${app.metadata.name}`, + {}, + {event: e} + ) + }>
    @@ -83,11 +92,11 @@ export const ApplicationsTable = (props: { /> - + {isApp(app) && }
    -
    Project:
    -
    {app.spec.project}
    + {isApp(app) &&
    Project:
    } + {isApp(app) &&
    {(app as models.Application).spec.project}
    }
    @@ -109,42 +118,50 @@ export const ApplicationsTable = (props: {
    -
    -
    -
    Source:
    -
    -
    - -
    -
    - + {isApp(app) && ( +
    +
    +
    Source:
    +
    +
    + +
    +
    + +
    -
    -
    -
    Destination:
    -
    - /{app.spec.destination.namespace} +
    +
    Destination:
    +
    + / + {(app as Application).spec.destination.namespace} +
    -
    - + )}
    - {app.status.health.status}
    - - {app.status.sync.status} - ( - - )} - items={[ - {title: 'Sync', action: () => props.syncApplication(app.metadata.name, app.metadata.namespace)}, - {title: 'Refresh', action: () => props.refreshApplication(app.metadata.name, app.metadata.namespace)}, - {title: 'Delete', action: () => props.deleteApplication(app.metadata.name, app.metadata.namespace)} - ]} - /> + {isApp(app) && }{' '} + {isApp(app) && {(app as Application).status.health.status}} {isApp(app) &&
    } + {!isApp(app) && }{' '} + {!isApp(app) && {getAppSetHealthStatus((app as ApplicationSet).status)}} {!isApp(app) &&
    } + {isApp(app) && } + {isApp(app) && {(app as Application).status.sync.status}}{' '} + {isApp(app) && } + {isApp(app) && ( + ( + + )} + items={[ + {title: 'Sync', action: () => props.syncApplication(app.metadata.name, app.metadata.namespace)}, + {title: 'Refresh', action: () => props.refreshApplication(app.metadata.name, app.metadata.namespace)}, + {title: 'Delete', action: () => props.deleteApplication(app.metadata.name, app.metadata.namespace)} + ]} + /> + )}
    diff --git a/ui/src/app/applications/components/applications-list/applications-tiles.tsx b/ui/src/app/applications/components/applications-list/applications-tiles.tsx index 3467d3b952a87..889770c51ea21 100644 --- a/ui/src/app/applications/components/applications-list/applications-tiles.tsx +++ b/ui/src/app/applications/components/applications-list/applications-tiles.tsx @@ -7,16 +7,27 @@ import {Consumer, Context, AuthSettingsCtx} from '../../../shared/context'; import * as models from '../../../shared/models'; import {ApplicationURLs} from '../application-urls'; import * as AppUtils from '../utils'; -import {getAppDefaultSource, OperationState} from '../utils'; +import {getAppDefaultSource, getAppSetHealthStatus, OperationState} from '../utils'; import {services} from '../../../shared/services'; import './applications-tiles.scss'; +import {Application, ApplicationSet} from '../../../shared/models'; +import {ResourceIcon} from '../resource-icon'; -export interface ApplicationTilesProps { +export interface ApplicationTilesProps extends AbstractApplicationTilesProps { applications: models.Application[]; syncApplication: (appName: string, appNamespace: string) => any; refreshApplication: (appName: string, appNamespace: string) => any; +} + +export interface AbstractApplicationTilesProps { + applications: models.AbstractApplication[]; deleteApplication: (appName: string, appNamespace: string) => any; + objectListKind: string; +} + +export interface ApplicationSetTilesProps extends AbstractApplicationTilesProps { + applications: models.ApplicationSet[]; } const useItemsPerContainer = (itemRef: any, containerRef: any): number => { @@ -46,8 +57,8 @@ const useItemsPerContainer = (itemRef: any, containerRef: any): number => { return itemsPer || 1; }; -export const ApplicationTiles = ({applications, syncApplication, refreshApplication, deleteApplication}: ApplicationTilesProps) => { - const [selectedApp, navApp, reset] = useNav(applications.length); +export const ApplicationTiles = (tilesProps: AbstractApplicationTilesProps) => { + const [selectedApp, navApp, reset] = useNav(tilesProps.applications.length); const ctxh = React.useContext(Context); const appRef = {ref: React.useRef(null), set: false}; @@ -56,6 +67,7 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat const useAuthSettingsCtx = React.useContext(AuthSettingsCtx); const {useKeybinding} = React.useContext(KeybindingContext); + const isListOfApplications = tilesProps.objectListKind === 'application'; useKeybinding({keys: Key.RIGHT, action: () => navApp(1)}); useKeybinding({keys: Key.LEFT, action: () => navApp(-1)}); @@ -66,7 +78,8 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat keys: Key.ENTER, action: () => { if (selectedApp > -1) { - ctxh.navigation.goto(`/applications/${applications[selectedApp].metadata.name}`); + ctxh.navigation.goto(`${AppUtils.getRootPathByPath(ctxh.history.location.pathname)}/${tilesProps.applications[selectedApp].metadata.name}`); + // tilesProps.applications[selectedApp] return true; } return false; @@ -106,65 +119,93 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat const favList = pref.appList.favoritesAppList || []; return (
    - {applications.map((app, i) => { + {tilesProps.applications.map((app, i) => { const source = getAppDefaultSource(app); return (
    + className={ + isListOfApplications + ? `argo-table-list__row applications-list__entry applications-list__entry--health-${ + (app as models.Application).status.health.status + } ${selectedApp === i ? 'applications-tiles__selected' : ''}` + : `argo-table-list__row applications-list__entry applications-list__entry--health-${getAppSetHealthStatus( + (app as ApplicationSet).status + )} ${selectedApp === i ? 'applications-tiles__selected' : ''}` + }>
    - ctx.navigation.goto(`/applications/${app.metadata.namespace}/${app.metadata.name}`, {view: pref.appDetails.view}, {event: e}) + ctx.navigation.goto( + `${AppUtils.getRootPathByPath(ctx.history.location.pathname)}/${app.metadata.namespace}/${app.metadata.name}`, + {view: pref.appDetails.view}, + {event: e} + ) }>
    -
    -
    0 ? 'columns small-10' : 'columns small-11'}> - - - - {AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled)} - - -
    -
    0 ? 'columns small-2' : 'columns small-1'}> -
    - - - +
    + {isListOfApplications && ( +
    0 ? 'columns small-10' : 'columns small-11'}> + + + + {AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled)} +
    -
    + )} + {!isListOfApplications && ( +
    + {'{'} + + {'}'} + + + {AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled)} + + +
    + )} + {isListOfApplications && ( +
    0 ? 'columns small-2' : 'columns small-1'}> +
    + + + + +
    +
    + )}
    -
    -
    - Project: + {isListOfApplications && ( +
    +
    + Project: +
    +
    {app.spec.project}
    -
    {app.spec.project}
    -
    + )}
    Labels: @@ -196,30 +237,38 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat Status:
    - {app.status.health.status} + {isListOfApplications && }{' '} + {isListOfApplications && (app as Application).status.health.status} + {!isListOfApplications && }{' '} + {!isListOfApplications && getAppSetHealthStatus((app as ApplicationSet).status)}   - {app.status.sync.status} + {isListOfApplications && }{' '} + {isListOfApplications && (app as Application).status.sync.status}   - + {isListOfApplications && }
    -
    -
    - Repository: -
    -
    - - {source.repoURL} - + {isListOfApplications && ( +
    +
    + Repository: +
    +
    + + {source.repoURL} + +
    -
    -
    -
    - Target Revision: + )} + {isListOfApplications && ( +
    +
    + Target Revision: +
    +
    {source.targetRevision || 'HEAD'}
    -
    {source.targetRevision || 'HEAD'}
    -
    - {source.path && ( + )} + {isListOfApplications && source.path && (
    Path: @@ -227,7 +276,7 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat
    {source.path}
    )} - {source.chart && ( + {isListOfApplications && source.chart && (
    Chart: @@ -235,71 +284,80 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat
    {source.chart}
    )} -
    -
    - Destination: -
    -
    - + {isListOfApplications && ( +
    +
    + Destination: +
    +
    + +
    -
    -
    -
    - Namespace: + )} + {isListOfApplications && ( +
    +
    + Namespace: +
    +
    {(app as models.Application).spec.destination.namespace}
    -
    {app.spec.destination.namespace}
    -
    + )}
    Created At:
    {AppUtils.formatCreationTimestamp(app.metadata.creationTimestamp)}
    - {app.status.operationState && ( + {isListOfApplications && (app as models.Application).status.operationState && (
    Last Sync:
    - {AppUtils.formatCreationTimestamp(app.status.operationState.finishedAt || app.status.operationState.startedAt)} + {AppUtils.formatCreationTimestamp( + (app as Application).status.operationState.finishedAt || app.status.operationState.startedAt + )}
    )} -
    -
    - { - e.stopPropagation(); - syncApplication(app.metadata.name, app.metadata.namespace); - }}> - Sync - -   - { - e.stopPropagation(); - refreshApplication(app.metadata.name, app.metadata.namespace); - }}> - {' '} - Refresh - -   - { - e.stopPropagation(); - deleteApplication(app.metadata.name, app.metadata.namespace); - }}> - Delete - + {isListOfApplications && ( + -
    + )}
    diff --git a/ui/src/app/applications/components/applications-refresh-panel/applications-refresh-panel.tsx b/ui/src/app/applications/components/applications-refresh-panel/applications-refresh-panel.tsx index 56cab7a39e982..1a9c3f67aa5b3 100644 --- a/ui/src/app/applications/components/applications-refresh-panel/applications-refresh-panel.tsx +++ b/ui/src/app/applications/components/applications-refresh-panel/applications-refresh-panel.tsx @@ -50,7 +50,7 @@ export const ApplicationsRefreshPanel = ({show, apps, hide}: {show: boolean; app const refreshActions = []; for (const app of selectedApps) { const refreshAction = async () => { - await services.applications.get(app.metadata.name, app.metadata.namespace, params.refreshType).catch(e => { + await services.applications.get(app.metadata.name, app.metadata.namespace, ctx.history.location.pathname, params.refreshType).catch(e => { ctx.notifications.show({ content: , type: NotificationType.Error @@ -95,7 +95,7 @@ export const ApplicationsRefreshPanel = ({show, apps, hide}: {show: boolean; app ))}
    - +
    )} diff --git a/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx b/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx index 0c97b75eb0b70..58e77d8bf318a 100644 --- a/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx +++ b/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx @@ -147,7 +147,7 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps: - +
    )} diff --git a/ui/src/app/applications/components/resource-details/resource-details.tsx b/ui/src/app/applications/components/resource-details/resource-details.tsx index 52d2fef184703..544d96449b525 100644 --- a/ui/src/app/applications/components/resource-details/resource-details.tsx +++ b/ui/src/app/applications/components/resource-details/resource-details.tsx @@ -5,7 +5,7 @@ import {EventsList, YamlEditor} from '../../../shared/components'; import * as models from '../../../shared/models'; import {ErrorBoundary} from '../../../shared/components/error-boundary/error-boundary'; import {Context} from '../../../shared/context'; -import {Application, ApplicationTree, AppSourceType, Event, RepoAppDetails, ResourceNode, State, SyncStatuses} from '../../../shared/models'; +import {AbstractApplication, Application, ApplicationTree, AppSourceType, Event, RepoAppDetails, ResourceNode, State, SyncStatuses} from '../../../shared/models'; import {services} from '../../../shared/services'; import {ResourceTabExtension} from '../../../shared/services/extensions-service'; import {NodeInfo, SelectNode} from '../application-details/application-details'; @@ -21,12 +21,13 @@ import {ResourceIcon} from '../resource-icon'; import {ResourceLabel} from '../resource-label'; import * as AppUtils from '../utils'; import './resource-details.scss'; +import {isApp} from '../utils'; const jsonMergePatch = require('json-merge-patch'); interface ResourceDetailsProps { selectedNode: ResourceNode; - updateApp: (app: Application, query: {validate?: boolean}) => Promise; + updateApp: (app: AbstractApplication, query: {validate?: boolean}) => Promise; application: Application; isAppSelected: boolean; tree: ApplicationTree; @@ -154,52 +155,55 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { }; const getApplicationTabs = () => { - const tabs: Tab[] = [ - { - title: 'SUMMARY', - key: 'summary', - content: updateApp(app, query)} /> - }, - { - title: 'PARAMETERS', - key: 'parameters', - content: ( - - services.repos.appDetails(AppUtils.getAppDefaultSource(app), app.metadata.name, app.spec.project).catch(() => ({ - type: 'Directory' as AppSourceType, - path: AppUtils.getAppDefaultSource(app).path - })) - }> - {(details: RepoAppDetails) => ( - updateApp(app, query)} - application={application} - details={details} - /> - )} - - ) - }, - { - title: 'MANIFEST', - key: 'manifest', - content: ( - { - const spec = JSON.parse(JSON.stringify(application.spec)); - return services.applications.updateSpec(application.metadata.name, application.metadata.namespace, jsonMergePatch.apply(spec, JSON.parse(patch))); - }} - /> - ) - } - ]; + const tabs: Tab[] = []; + if (isApp(application)) { + tabs.push( + { + title: 'SUMMARY', + key: 'summary', + content: updateApp(app, query)} /> + }, + { + title: 'PARAMETERS', + key: 'parameters', + content: ( + + services.repos.appDetails(AppUtils.getAppDefaultSource(app), app.metadata.name, app.spec.project).catch(() => ({ + type: 'Directory' as AppSourceType, + path: AppUtils.getAppDefaultSource(app).path + })) + }> + {(details: RepoAppDetails) => ( + updateApp(app, query)} + application={application} + details={details} + /> + )} + + ) + } + ); + } + tabs.push({ + title: 'MANIFEST', + key: 'manifest', + content: ( + { + const spec = JSON.parse(JSON.stringify(application.spec)); + return services.applications.updateSpec(application.metadata.name, application.metadata.namespace, jsonMergePatch.apply(spec, JSON.parse(patch))); + }} + /> + ) + }); - if (application.status.sync.status !== SyncStatuses.Synced) { + if (isApp(application) && (application as Application).status.sync.status !== SyncStatuses.Synced) { tabs.push({ icon: 'fa fa-file-medical', title: 'DIFF', @@ -208,7 +212,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { - await services.applications.managedResources(application.metadata.name, application.metadata.namespace, { + await services.applications.managedResources(application.metadata.name, application.metadata.namespace, appContext.history.location.pathname, { fields: ['items.normalizedLiveState', 'items.predictedLiveState', 'items.group', 'items.kind', 'items.namespace', 'items.name'] }) }> @@ -224,7 +228,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { content: }); - const extensionTabs = services.extensions.getResourceTabs('argoproj.io', 'Application').map((ext, i) => ({ + const extensionTabs = services.extensions.getResourceTabs('argoproj.io', application.kind).map((ext, i) => ({ title: ext.title, key: `extension-${i}`, content: , @@ -243,14 +247,19 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { noLoaderOnInputChange={true} input={selectedNode.resourceVersion} load={async () => { - const managedResources = await services.applications.managedResources(application.metadata.name, application.metadata.namespace, { - id: { - name: selectedNode.name, - namespace: selectedNode.namespace, - kind: selectedNode.kind, - group: selectedNode.group + const managedResources = await services.applications.managedResources( + application.metadata.name, + application.metadata.namespace, + appContext.history.location.pathname, + { + id: { + name: selectedNode.name, + namespace: selectedNode.namespace, + kind: selectedNode.kind, + group: selectedNode.group + } } - }); + ); const controlled = managedResources.find(item => AppUtils.isSameNode(selectedNode, item)); const summary = application.status.resources.find(item => AppUtils.isSameNode(selectedNode, item)); const controlledState = (controlled && summary && {summary, state: controlled}) || null; @@ -258,7 +267,9 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { if (controlled && controlled.targetState) { resQuery.version = AppUtils.parseApiVersion(controlled.targetState.apiVersion).version; } - const liveState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, resQuery).catch(() => null); + const liveState = await services.applications + .getResource(application.metadata.name, application.metadata.namespace, appContext.history.location.pathname, resQuery) + .catch(() => null); const events = (liveState && (await services.applications.resourceEvents(application.metadata.name, application.metadata.namespace, { @@ -273,7 +284,9 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { } else { const childPod = AppUtils.findChildPod(selectedNode, tree); if (childPod) { - podState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, childPod).catch(() => null); + podState = await services.applications + .getResource(application.metadata.name, application.metadata.namespace, appContext.history.location.pathname, childPod) + .catch(() => null); } } @@ -307,7 +320,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { SYNC