commit
2330011e96
|
@ -19,6 +19,7 @@ type Client struct {
|
|||
SourcesStore *SourcesStore
|
||||
ServersStore *ServersStore
|
||||
LayoutStore *LayoutStore
|
||||
UsersStore *UsersStore
|
||||
AlertsStore *AlertsStore
|
||||
}
|
||||
|
||||
|
@ -28,6 +29,7 @@ func NewClient() *Client {
|
|||
c.SourcesStore = &SourcesStore{client: c}
|
||||
c.ServersStore = &ServersStore{client: c}
|
||||
c.AlertsStore = &AlertsStore{client: c}
|
||||
c.UsersStore = &UsersStore{client: c}
|
||||
c.LayoutStore = &LayoutStore{
|
||||
client: c,
|
||||
IDs: &uuid.V4{},
|
||||
|
@ -65,6 +67,10 @@ func (c *Client) Open() error {
|
|||
if _, err := tx.CreateBucketIfNotExists(AlertsBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
// Always create Users bucket.
|
||||
if _, err := tx.CreateBucketIfNotExists(UsersBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
|
|
|
@ -205,3 +205,23 @@ func UnmarshalAlertRule(data []byte, r *ScopedAlert) error {
|
|||
r.KapaID = int(pb.KapaID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalUser encodes a user to binary protobuf format.
|
||||
func MarshalUser(u *chronograf.User) ([]byte, error) {
|
||||
return proto.Marshal(&User{
|
||||
ID: uint64(u.ID),
|
||||
Email: u.Email,
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalUser decodes a user from binary protobuf data.
|
||||
func UnmarshalUser(data []byte, u *chronograf.User) error {
|
||||
var pb User
|
||||
if err := proto.Unmarshal(data, &pb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.ID = chronograf.UserID(pb.ID)
|
||||
u.Email = pb.Email
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ It has these top-level messages:
|
|||
Cell
|
||||
Query
|
||||
AlertRule
|
||||
User
|
||||
*/
|
||||
package internal
|
||||
|
||||
|
@ -35,13 +36,13 @@ var _ = math.Inf
|
|||
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type Exploration struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,json=name,proto3" json:"Name,omitempty"`
|
||||
UserID int64 `protobuf:"varint,3,opt,name=UserID,json=userID,proto3" json:"UserID,omitempty"`
|
||||
Data string `protobuf:"bytes,4,opt,name=Data,json=data,proto3" json:"Data,omitempty"`
|
||||
CreatedAt int64 `protobuf:"varint,5,opt,name=CreatedAt,json=createdAt,proto3" json:"CreatedAt,omitempty"`
|
||||
UpdatedAt int64 `protobuf:"varint,6,opt,name=UpdatedAt,json=updatedAt,proto3" json:"UpdatedAt,omitempty"`
|
||||
Default bool `protobuf:"varint,7,opt,name=Default,json=default,proto3" json:"Default,omitempty"`
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
UserID int64 `protobuf:"varint,3,opt,name=UserID,proto3" json:"UserID,omitempty"`
|
||||
Data string `protobuf:"bytes,4,opt,name=Data,proto3" json:"Data,omitempty"`
|
||||
CreatedAt int64 `protobuf:"varint,5,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"`
|
||||
UpdatedAt int64 `protobuf:"varint,6,opt,name=UpdatedAt,proto3" json:"UpdatedAt,omitempty"`
|
||||
Default bool `protobuf:"varint,7,opt,name=Default,proto3" json:"Default,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Exploration) Reset() { *m = Exploration{} }
|
||||
|
@ -50,14 +51,14 @@ func (*Exploration) ProtoMessage() {}
|
|||
func (*Exploration) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
|
||||
|
||||
type Source struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,json=name,proto3" json:"Name,omitempty"`
|
||||
Type string `protobuf:"bytes,3,opt,name=Type,json=type,proto3" json:"Type,omitempty"`
|
||||
Username string `protobuf:"bytes,4,opt,name=Username,json=username,proto3" json:"Username,omitempty"`
|
||||
Password string `protobuf:"bytes,5,opt,name=Password,json=password,proto3" json:"Password,omitempty"`
|
||||
URL string `protobuf:"bytes,6,opt,name=URL,json=uRL,proto3" json:"URL,omitempty"`
|
||||
Default bool `protobuf:"varint,7,opt,name=Default,json=default,proto3" json:"Default,omitempty"`
|
||||
Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,json=telegraf,proto3" json:"Telegraf,omitempty"`
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Type string `protobuf:"bytes,3,opt,name=Type,proto3" json:"Type,omitempty"`
|
||||
Username string `protobuf:"bytes,4,opt,name=Username,proto3" json:"Username,omitempty"`
|
||||
Password string `protobuf:"bytes,5,opt,name=Password,proto3" json:"Password,omitempty"`
|
||||
URL string `protobuf:"bytes,6,opt,name=URL,proto3" json:"URL,omitempty"`
|
||||
Default bool `protobuf:"varint,7,opt,name=Default,proto3" json:"Default,omitempty"`
|
||||
Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,proto3" json:"Telegraf,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Source) Reset() { *m = Source{} }
|
||||
|
@ -66,12 +67,12 @@ func (*Source) ProtoMessage() {}
|
|||
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{1} }
|
||||
|
||||
type Server struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,json=name,proto3" json:"Name,omitempty"`
|
||||
Username string `protobuf:"bytes,3,opt,name=Username,json=username,proto3" json:"Username,omitempty"`
|
||||
Password string `protobuf:"bytes,4,opt,name=Password,json=password,proto3" json:"Password,omitempty"`
|
||||
URL string `protobuf:"bytes,5,opt,name=URL,json=uRL,proto3" json:"URL,omitempty"`
|
||||
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,json=srcID,proto3" json:"SrcID,omitempty"`
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"`
|
||||
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
|
||||
URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"`
|
||||
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Server) Reset() { *m = Server{} }
|
||||
|
@ -80,10 +81,10 @@ func (*Server) ProtoMessage() {}
|
|||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{2} }
|
||||
|
||||
type Layout struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
|
||||
Application string `protobuf:"bytes,2,opt,name=Application,json=application,proto3" json:"Application,omitempty"`
|
||||
Measurement string `protobuf:"bytes,3,opt,name=Measurement,json=measurement,proto3" json:"Measurement,omitempty"`
|
||||
Cells []*Cell `protobuf:"bytes,4,rep,name=Cells,json=cells" json:"Cells,omitempty"`
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Application string `protobuf:"bytes,2,opt,name=Application,proto3" json:"Application,omitempty"`
|
||||
Measurement string `protobuf:"bytes,3,opt,name=Measurement,proto3" json:"Measurement,omitempty"`
|
||||
Cells []*Cell `protobuf:"bytes,4,rep,name=Cells" json:"Cells,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Layout) Reset() { *m = Layout{} }
|
||||
|
@ -121,11 +122,11 @@ func (m *Cell) GetQueries() []*Query {
|
|||
}
|
||||
|
||||
type Query struct {
|
||||
Command string `protobuf:"bytes,1,opt,name=Command,json=command,proto3" json:"Command,omitempty"`
|
||||
DB string `protobuf:"bytes,2,opt,name=DB,json=dB,proto3" json:"DB,omitempty"`
|
||||
RP string `protobuf:"bytes,3,opt,name=RP,json=rP,proto3" json:"RP,omitempty"`
|
||||
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys,json=groupBys" json:"GroupBys,omitempty"`
|
||||
Wheres []string `protobuf:"bytes,5,rep,name=Wheres,json=wheres" json:"Wheres,omitempty"`
|
||||
Command string `protobuf:"bytes,1,opt,name=Command,proto3" json:"Command,omitempty"`
|
||||
DB string `protobuf:"bytes,2,opt,name=DB,proto3" json:"DB,omitempty"`
|
||||
RP string `protobuf:"bytes,3,opt,name=RP,proto3" json:"RP,omitempty"`
|
||||
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys" json:"GroupBys,omitempty"`
|
||||
Wheres []string `protobuf:"bytes,5,rep,name=Wheres" json:"Wheres,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Query) Reset() { *m = Query{} }
|
||||
|
@ -134,10 +135,10 @@ func (*Query) ProtoMessage() {}
|
|||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
|
||||
type AlertRule struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
|
||||
JSON string `protobuf:"bytes,2,opt,name=JSON,json=jSON,proto3" json:"JSON,omitempty"`
|
||||
SrcID int64 `protobuf:"varint,3,opt,name=SrcID,json=srcID,proto3" json:"SrcID,omitempty"`
|
||||
KapaID int64 `protobuf:"varint,4,opt,name=KapaID,json=kapaID,proto3" json:"KapaID,omitempty"`
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
JSON string `protobuf:"bytes,2,opt,name=JSON,proto3" json:"JSON,omitempty"`
|
||||
SrcID int64 `protobuf:"varint,3,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
|
||||
KapaID int64 `protobuf:"varint,4,opt,name=KapaID,proto3" json:"KapaID,omitempty"`
|
||||
}
|
||||
|
||||
func (m *AlertRule) Reset() { *m = AlertRule{} }
|
||||
|
@ -145,6 +146,16 @@ func (m *AlertRule) String() string { return proto.CompactTextString(
|
|||
func (*AlertRule) ProtoMessage() {}
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
|
||||
type User struct {
|
||||
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Email string `protobuf:"bytes,2,opt,name=Email,proto3" json:"Email,omitempty"`
|
||||
}
|
||||
|
||||
func (m *User) Reset() { *m = User{} }
|
||||
func (m *User) String() string { return proto.CompactTextString(m) }
|
||||
func (*User) ProtoMessage() {}
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Exploration)(nil), "internal.Exploration")
|
||||
proto.RegisterType((*Source)(nil), "internal.Source")
|
||||
|
@ -153,45 +164,45 @@ func init() {
|
|||
proto.RegisterType((*Cell)(nil), "internal.Cell")
|
||||
proto.RegisterType((*Query)(nil), "internal.Query")
|
||||
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
|
||||
proto.RegisterType((*User)(nil), "internal.User")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 547 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x94, 0xcd, 0x8e, 0xd3, 0x3e,
|
||||
0x10, 0xc0, 0xe5, 0x26, 0xce, 0x87, 0xfb, 0xd7, 0xfe, 0x91, 0x85, 0x50, 0x84, 0x38, 0x54, 0x11,
|
||||
0x87, 0x72, 0xd9, 0x03, 0x3c, 0x41, 0xdb, 0x20, 0x54, 0x28, 0xdd, 0xe2, 0x6e, 0xc5, 0x89, 0x83,
|
||||
0x49, 0x66, 0xb7, 0x81, 0x7c, 0xe1, 0xd8, 0xb4, 0xb9, 0x72, 0x85, 0xb7, 0xe1, 0x05, 0x78, 0x34,
|
||||
0x64, 0xc7, 0x25, 0x95, 0xf8, 0xd0, 0x1e, 0x7f, 0x33, 0x13, 0xfb, 0x37, 0x33, 0x6e, 0xc9, 0x45,
|
||||
0x5e, 0x49, 0x10, 0x15, 0x2f, 0x2e, 0x1b, 0x51, 0xcb, 0x9a, 0x06, 0x27, 0x8e, 0xbf, 0x23, 0x32,
|
||||
0x7e, 0x7e, 0x6c, 0x8a, 0x5a, 0x70, 0x99, 0xd7, 0x15, 0xbd, 0x20, 0xa3, 0x65, 0x12, 0xa1, 0x09,
|
||||
0x9a, 0x3a, 0x6c, 0x94, 0x27, 0x94, 0x12, 0x77, 0xcd, 0x4b, 0x88, 0x46, 0x13, 0x34, 0x0d, 0x99,
|
||||
0x5b, 0xf1, 0x12, 0xe8, 0x03, 0xe2, 0xed, 0x5a, 0x10, 0xcb, 0x24, 0x72, 0x4c, 0x9d, 0xa7, 0x0c,
|
||||
0xe9, 0xda, 0x84, 0x4b, 0x1e, 0xb9, 0x7d, 0x6d, 0xc6, 0x25, 0xa7, 0x8f, 0x48, 0xb8, 0x10, 0xc0,
|
||||
0x25, 0x64, 0x33, 0x19, 0x61, 0x53, 0x1e, 0xa6, 0xa7, 0x80, 0xce, 0xee, 0x9a, 0xcc, 0x66, 0xbd,
|
||||
0x3e, 0xab, 0x4e, 0x01, 0x1a, 0x11, 0x3f, 0x81, 0x1b, 0xae, 0x0a, 0x19, 0xf9, 0x13, 0x34, 0x0d,
|
||||
0x98, 0x9f, 0xf5, 0x18, 0xff, 0x40, 0xc4, 0xdb, 0xd6, 0x4a, 0xa4, 0x70, 0x27, 0x61, 0x4a, 0xdc,
|
||||
0xeb, 0xae, 0x01, 0xa3, 0x1b, 0x32, 0x57, 0x76, 0x0d, 0xd0, 0x87, 0x24, 0xd0, 0x4d, 0xe8, 0xbc,
|
||||
0x15, 0x0e, 0x94, 0x65, 0x9d, 0xdb, 0xf0, 0xb6, 0x3d, 0xd4, 0x22, 0x33, 0xce, 0x21, 0x0b, 0x1a,
|
||||
0xcb, 0xf4, 0x1e, 0x71, 0x76, 0x6c, 0x65, 0x64, 0x43, 0xe6, 0x28, 0xb6, 0xfa, 0xbb, 0xa6, 0x3e,
|
||||
0xe7, 0x1a, 0x0a, 0xb8, 0x15, 0xfc, 0x26, 0x0a, 0xfa, 0x73, 0xa4, 0xe5, 0xf8, 0x9b, 0x6e, 0x01,
|
||||
0xc4, 0x67, 0x10, 0x77, 0x6a, 0xe1, 0x5c, 0xd7, 0xf9, 0x87, 0xae, 0xfb, 0x67, 0x5d, 0x3c, 0xe8,
|
||||
0xde, 0x27, 0x78, 0x2b, 0xd2, 0x65, 0x62, 0xe7, 0x8d, 0x5b, 0x0d, 0xf1, 0x17, 0x44, 0xbc, 0x15,
|
||||
0xef, 0x6a, 0x25, 0xcf, 0x74, 0x42, 0xa3, 0x33, 0x21, 0xe3, 0x59, 0xd3, 0x14, 0x79, 0x6a, 0x5e,
|
||||
0x88, 0xb5, 0x1a, 0xf3, 0x21, 0xa4, 0x2b, 0x5e, 0x03, 0x6f, 0x95, 0x80, 0x12, 0x2a, 0x69, 0xfd,
|
||||
0xc6, 0xe5, 0x10, 0xa2, 0x8f, 0x09, 0x5e, 0x40, 0x51, 0xb4, 0x91, 0x3b, 0x71, 0xa6, 0xe3, 0xa7,
|
||||
0x17, 0x97, 0xbf, 0x1e, 0xa4, 0x0e, 0x33, 0x9c, 0xea, 0x64, 0xfc, 0x15, 0x11, 0x57, 0x33, 0xfd,
|
||||
0x8f, 0xa0, 0xa3, 0x31, 0xc0, 0x0c, 0x1d, 0x35, 0x75, 0xe6, 0x5a, 0xcc, 0x50, 0xa7, 0xe9, 0x60,
|
||||
0xae, 0xc0, 0x0c, 0x1d, 0x34, 0xed, 0x4d, 0xd3, 0x98, 0xa1, 0x3d, 0x7d, 0x42, 0xfc, 0x4f, 0x0a,
|
||||
0x44, 0x0e, 0x6d, 0x84, 0xcd, 0x45, 0xff, 0x0f, 0x17, 0xbd, 0x51, 0x20, 0x3a, 0x76, 0xca, 0xeb,
|
||||
0x0f, 0x73, 0xbb, 0x45, 0x94, 0xeb, 0x91, 0x9b, 0xd1, 0xfa, 0xc3, 0xc8, 0x63, 0x45, 0xb0, 0xf9,
|
||||
0x46, 0x2f, 0x78, 0x51, 0x97, 0x25, 0xaf, 0x32, 0x3b, 0x15, 0x3f, 0xed, 0x51, 0x8f, 0x2a, 0x99,
|
||||
0xdb, 0x89, 0x8c, 0xb2, 0xb9, 0x66, 0xb6, 0xb1, 0xfd, 0x8f, 0xc4, 0x46, 0x6f, 0xe6, 0x85, 0xa8,
|
||||
0x55, 0x33, 0xef, 0xfa, 0xce, 0x43, 0x16, 0xdc, 0x5a, 0xd6, 0xbf, 0xa2, 0xb7, 0x7b, 0x10, 0x56,
|
||||
0x35, 0x64, 0xde, 0xc1, 0x50, 0xfc, 0x8e, 0x84, 0xb3, 0x02, 0x84, 0x64, 0xaa, 0x80, 0xdf, 0x76,
|
||||
0x41, 0x89, 0xfb, 0x72, 0x7b, 0xb5, 0x3e, 0x3d, 0x8d, 0x0f, 0xdb, 0xab, 0xf5, 0xb0, 0x50, 0xe7,
|
||||
0x6c, 0xa1, 0xfa, 0xf8, 0x57, 0xbc, 0xe1, 0xcb, 0xc4, 0x4c, 0xc7, 0x61, 0xde, 0x47, 0x43, 0xef,
|
||||
0x3d, 0xf3, 0x0f, 0xf0, 0xec, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xad, 0x5d, 0x44, 0xfb, 0x13,
|
||||
0x04, 0x00, 0x00,
|
||||
// 541 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x94, 0x4b, 0x8e, 0xd3, 0x4c,
|
||||
0x10, 0xc7, 0xd5, 0xb1, 0x3b, 0x89, 0x2b, 0x9f, 0xf2, 0xa1, 0xd6, 0x08, 0x59, 0x88, 0x45, 0x64,
|
||||
0xb1, 0x08, 0x12, 0x9a, 0x05, 0x9c, 0x20, 0x89, 0x47, 0x28, 0x30, 0x0c, 0xa1, 0x33, 0x11, 0x2b,
|
||||
0x16, 0x4d, 0x52, 0x43, 0x2c, 0x39, 0xb6, 0x69, 0xdb, 0x24, 0xde, 0xb2, 0x85, 0xdb, 0x70, 0x01,
|
||||
0x8e, 0x86, 0xaa, 0xdd, 0x76, 0x2c, 0xf1, 0xd0, 0xec, 0xea, 0x5f, 0x55, 0xae, 0xfe, 0xd5, 0x23,
|
||||
0x81, 0x71, 0x94, 0x14, 0xa8, 0x13, 0x15, 0x5f, 0x66, 0x3a, 0x2d, 0x52, 0x31, 0x6c, 0x74, 0xf0,
|
||||
0x83, 0xc1, 0xe8, 0xea, 0x94, 0xc5, 0xa9, 0x56, 0x45, 0x94, 0x26, 0x62, 0x0c, 0xbd, 0x65, 0xe8,
|
||||
0xb3, 0x09, 0x9b, 0x3a, 0xb2, 0xb7, 0x0c, 0x85, 0x00, 0xf7, 0x46, 0x1d, 0xd0, 0xef, 0x4d, 0xd8,
|
||||
0xd4, 0x93, 0xc6, 0x16, 0x0f, 0xa1, 0xbf, 0xc9, 0x51, 0x2f, 0x43, 0xdf, 0x31, 0x79, 0x56, 0x51,
|
||||
0x6e, 0xa8, 0x0a, 0xe5, 0xbb, 0x75, 0x2e, 0xd9, 0xe2, 0x31, 0x78, 0x0b, 0x8d, 0xaa, 0xc0, 0xdd,
|
||||
0xac, 0xf0, 0xb9, 0x49, 0x3f, 0x3b, 0x28, 0xba, 0xc9, 0x76, 0x36, 0xda, 0xaf, 0xa3, 0xad, 0x43,
|
||||
0xf8, 0x30, 0x08, 0xf1, 0x4e, 0x95, 0x71, 0xe1, 0x0f, 0x26, 0x6c, 0x3a, 0x94, 0x8d, 0x0c, 0x7e,
|
||||
0x32, 0xe8, 0xaf, 0xd3, 0x52, 0x6f, 0xf1, 0x5e, 0xc0, 0x02, 0xdc, 0xdb, 0x2a, 0x43, 0x83, 0xeb,
|
||||
0x49, 0x63, 0x8b, 0x47, 0x30, 0x24, 0xec, 0x84, 0x72, 0x6b, 0xe0, 0x56, 0x53, 0x6c, 0xa5, 0xf2,
|
||||
0xfc, 0x98, 0xea, 0x9d, 0x61, 0xf6, 0x64, 0xab, 0xc5, 0x03, 0x70, 0x36, 0xf2, 0xda, 0xc0, 0x7a,
|
||||
0x92, 0xcc, 0xbf, 0x63, 0x52, 0x9d, 0x5b, 0x8c, 0xf1, 0x93, 0x56, 0x77, 0xfe, 0xb0, 0xae, 0xd3,
|
||||
0xe8, 0xe0, 0x3b, 0xb5, 0x80, 0xfa, 0x0b, 0xea, 0x7b, 0xb5, 0xd0, 0xc5, 0x75, 0xfe, 0x81, 0xeb,
|
||||
0xfe, 0x19, 0x97, 0x9f, 0x71, 0x2f, 0x80, 0xaf, 0xf5, 0x76, 0x19, 0xda, 0x79, 0xd7, 0x22, 0xf8,
|
||||
0xca, 0xa0, 0x7f, 0xad, 0xaa, 0xb4, 0x2c, 0x3a, 0x38, 0x9e, 0xc1, 0x99, 0xc0, 0x68, 0x96, 0x65,
|
||||
0x71, 0xb4, 0x35, 0x17, 0x62, 0xa9, 0xba, 0x2e, 0xca, 0x78, 0x83, 0x2a, 0x2f, 0x35, 0x1e, 0x30,
|
||||
0x29, 0x2c, 0x5f, 0xd7, 0x25, 0x9e, 0x00, 0x5f, 0x60, 0x1c, 0xe7, 0xbe, 0x3b, 0x71, 0xa6, 0xa3,
|
||||
0xe7, 0xe3, 0xcb, 0xf6, 0x20, 0xc9, 0x2d, 0xeb, 0x60, 0xf0, 0x8d, 0x81, 0x4b, 0x96, 0xf8, 0x0f,
|
||||
0xd8, 0xc9, 0x10, 0x70, 0xc9, 0x4e, 0xa4, 0x2a, 0xf3, 0x2c, 0x97, 0xac, 0x22, 0x75, 0x34, 0x4f,
|
||||
0x70, 0xc9, 0x8e, 0xa4, 0xf6, 0xa6, 0x69, 0x2e, 0xd9, 0x5e, 0x3c, 0x85, 0xc1, 0xe7, 0x12, 0x75,
|
||||
0x84, 0xb9, 0xcf, 0xcd, 0x43, 0xff, 0x9f, 0x1f, 0x7a, 0x57, 0xa2, 0xae, 0x64, 0x13, 0xa7, 0x0f,
|
||||
0x23, 0xbb, 0x45, 0x16, 0xd1, 0xc8, 0xcd, 0x68, 0x07, 0xf5, 0xc8, 0xc9, 0x0e, 0x4a, 0xe0, 0xe6,
|
||||
0x1b, 0x5a, 0xf0, 0x22, 0x3d, 0x1c, 0x54, 0xb2, 0xb3, 0x53, 0x69, 0x24, 0x8d, 0x2a, 0x9c, 0xdb,
|
||||
0x89, 0xf4, 0xc2, 0x39, 0x69, 0xb9, 0xb2, 0xfd, 0xf7, 0xe4, 0x8a, 0x36, 0xf3, 0x52, 0xa7, 0x65,
|
||||
0x36, 0xaf, 0xea, 0xce, 0x3d, 0xd9, 0x6a, 0xfa, 0x15, 0xbd, 0xdf, 0xa3, 0xb6, 0xa8, 0x9e, 0xb4,
|
||||
0x2a, 0xf8, 0x00, 0xde, 0x2c, 0x46, 0x5d, 0xc8, 0x32, 0xc6, 0xdf, 0x76, 0x21, 0xc0, 0x7d, 0xb5,
|
||||
0x7e, 0x7b, 0xd3, 0x9c, 0x06, 0xd9, 0xe7, 0x85, 0x3a, 0x9d, 0x85, 0x52, 0xf9, 0xd7, 0x2a, 0x53,
|
||||
0xcb, 0xd0, 0x4c, 0xc7, 0x91, 0x56, 0x05, 0xcf, 0xc0, 0xa5, 0xc3, 0xe9, 0x54, 0x76, 0x4d, 0xe5,
|
||||
0x0b, 0xe0, 0x57, 0x07, 0x15, 0xc5, 0xb6, 0x74, 0x2d, 0x3e, 0xf6, 0xcd, 0xff, 0xc5, 0x8b, 0x5f,
|
||||
0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x2c, 0x30, 0x90, 0x41, 0x04, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -62,3 +62,8 @@ message AlertRule {
|
|||
int64 SrcID = 3; // SrcID is the id of the source this alert is associated with
|
||||
int64 KapaID = 4; // KapaID is the id of the kapacitor this alert is associated with
|
||||
}
|
||||
|
||||
message User {
|
||||
uint64 ID = 1; // ID is the unique ID of this user
|
||||
string Email = 2; // Email byte representation of the user
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt/internal"
|
||||
)
|
||||
|
||||
// Ensure UsersStore implements chronograf.UsersStore.
|
||||
var _ chronograf.UsersStore = &UsersStore{}
|
||||
|
||||
var UsersBucket = []byte("Users")
|
||||
|
||||
type UsersStore struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// FindByEmail searches the UsersStore for all users owned with the email
|
||||
func (s *UsersStore) FindByEmail(ctx context.Context, email string) (*chronograf.User, error) {
|
||||
var user chronograf.User
|
||||
err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
err := tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
|
||||
var u chronograf.User
|
||||
if err := internal.UnmarshalUser(v, &u); err != nil {
|
||||
return err
|
||||
} else if u.Email != email {
|
||||
return nil
|
||||
}
|
||||
user.Email = u.Email
|
||||
user.ID = u.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.ID == 0 {
|
||||
return chronograf.ErrUserNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Create a new Users in the UsersStore.
|
||||
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(UsersBucket)
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ID = chronograf.UserID(seq)
|
||||
|
||||
if v, err := internal.MarshalUser(u); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(int(u.ID)), v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Delete the users from the UsersStore
|
||||
func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error {
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
if err := tx.Bucket(UsersBucket).Delete(itob(int(u.ID))); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a user by id.
|
||||
func (s *UsersStore) Get(ctx context.Context, id chronograf.UserID) (*chronograf.User, error) {
|
||||
var u chronograf.User
|
||||
if err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
if v := tx.Bucket(UsersBucket).Get(itob(int(id))); v == nil {
|
||||
return chronograf.ErrUserNotFound
|
||||
} else if err := internal.UnmarshalUser(v, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// Update a user
|
||||
func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
// Retrieve an existing user with the same ID.
|
||||
var u chronograf.User
|
||||
b := tx.Bucket(UsersBucket)
|
||||
if v := b.Get(itob(int(usr.ID))); v == nil {
|
||||
return chronograf.ErrUserNotFound
|
||||
} else if err := internal.UnmarshalUser(v, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Email = usr.Email
|
||||
|
||||
if v, err := internal.MarshalUser(&u); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(int(u.ID)), v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -13,6 +13,7 @@ const (
|
|||
ErrSourceNotFound = Error("source not found")
|
||||
ErrServerNotFound = Error("server not found")
|
||||
ErrLayoutNotFound = Error("layout not found")
|
||||
ErrUserNotFound = Error("user not found")
|
||||
ErrLayoutInvalid = Error("layout is invalid")
|
||||
ErrAlertNotFound = Error("alert not found")
|
||||
ErrAuthentication = Error("user not authenticated")
|
||||
|
@ -196,23 +197,22 @@ type UserID int
|
|||
|
||||
// User represents an authenticated user.
|
||||
type User struct {
|
||||
ID UserID
|
||||
Name string
|
||||
ID UserID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// AuthStore is the Storage and retrieval of authentication information
|
||||
type AuthStore struct {
|
||||
// User management for the AuthStore
|
||||
Users interface {
|
||||
// Create a new User in the AuthStore
|
||||
Add(context.Context, User) error
|
||||
// Delete the User from the AuthStore
|
||||
Delete(context.Context, User) error
|
||||
// Retrieve a user if `ID` exists.
|
||||
Get(ctx context.Context, ID int) error
|
||||
// UsersStore is the Storage and retrieval of authentication information
|
||||
type UsersStore interface {
|
||||
// Create a new User in the UsersStore
|
||||
Add(context.Context, *User) (*User, error)
|
||||
// Delete the User from the UsersStore
|
||||
Delete(context.Context, *User) error
|
||||
// Get retrieves a user if `ID` exists.
|
||||
Get(ctx context.Context, ID UserID) (*User, error)
|
||||
// Update the user's permissions or roles
|
||||
Update(context.Context, User) error
|
||||
}
|
||||
Update(context.Context, *User) error
|
||||
// FindByEmail will retrieve a user by email address.
|
||||
FindByEmail(ctx context.Context, Email string) (*User, error)
|
||||
}
|
||||
|
||||
// ExplorationID is a unique ID for an Exploration.
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
download a binary here https://github.com/google/protobuf/releases/tag/v3.1.0
|
||||
|
||||
run the following 4 commands listed here https://github.com/gogo/protobuf
|
||||
```sh
|
||||
go get github.com/gogo/protobuf/proto
|
||||
go get github.com/gogo/protobuf/jsonpb
|
||||
go get github.com/gogo/protobuf/protoc-gen-gogo
|
||||
go get github.com/gogo/protobuf/gogoproto
|
||||
```
|
|
@ -94,14 +94,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.DELETE("/chronograf/v1/layouts/:id", service.RemoveLayout)
|
||||
|
||||
// Users
|
||||
/*
|
||||
router.GET("/chronograf/v1/users", Users)
|
||||
router.POST("/chronograf/v1/users", NewUser)
|
||||
router.GET("/chronograf/v1/me", service.Me)
|
||||
router.POST("/chronograf/v1/users", service.NewUser)
|
||||
|
||||
router.GET("/chronograf/v1/users/:id", service.UserID)
|
||||
router.PATCH("/chronograf/v1/users/:id", service.UpdateUser)
|
||||
router.DELETE("/chronograf/v1/users/:id", service.RemoveUser)
|
||||
|
||||
router.GET("/chronograf/v1/users/:id", UsersID)
|
||||
router.PATCH("/chronograf/v1/users/:id", UpdateUser)
|
||||
router.DELETE("/chronograf/v1/users/:id", RemoveUser)
|
||||
*/
|
||||
// Explorations
|
||||
router.GET("/chronograf/v1/users/:id/explorations", service.Explorations)
|
||||
router.POST("/chronograf/v1/users/:id/explorations", service.NewExploration)
|
||||
|
@ -133,7 +132,7 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
|
|||
opts.Logger,
|
||||
)
|
||||
|
||||
router.GET("/oauth", gh.Login())
|
||||
router.GET("/oauth/github", gh.Login())
|
||||
router.GET("/oauth/logout", gh.Logout())
|
||||
router.GET("/oauth/github/callback", gh.Callback())
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ type getRoutesResponse struct {
|
|||
Mappings string `json:"mappings"` // Location of the application mappings endpoint
|
||||
Sources string `json:"sources"` // Location of the sources endpoint
|
||||
Users string `json:"users"` // Location of the users endpoint
|
||||
Me string `json:"me"` // Location of the me endpoint
|
||||
}
|
||||
|
||||
// AllRoutes returns all top level routes within chronograf
|
||||
|
@ -19,6 +20,7 @@ func AllRoutes(logger chronograf.Logger) http.HandlerFunc {
|
|||
Sources: "/chronograf/v1/sources",
|
||||
Layouts: "/chronograf/v1/layouts",
|
||||
Users: "/chronograf/v1/users",
|
||||
Me: "/chronograf/v1/me",
|
||||
Mappings: "/chronograf/v1/mappings",
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ func (s *Server) useAuth() bool {
|
|||
// Serve starts and runs the chronograf server
|
||||
func (s *Server) Serve() error {
|
||||
logger := clog.New(clog.ParseLevel(s.LogLevel))
|
||||
service := openService(s.BoltPath, s.CannedPath, logger)
|
||||
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
|
||||
s.handler = NewMux(MuxOpts{
|
||||
Develop: s.Develop,
|
||||
TokenSecret: s.TokenSecret,
|
||||
|
@ -106,7 +106,7 @@ func (s *Server) Serve() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func openService(boltPath, cannedPath string, logger chronograf.Logger) Service {
|
||||
func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth bool) Service {
|
||||
db := bolt.NewClient()
|
||||
db.Path = boltPath
|
||||
if err := db.Open(); err != nil {
|
||||
|
@ -137,12 +137,14 @@ func openService(boltPath, cannedPath string, logger chronograf.Logger) Service
|
|||
ExplorationStore: db.ExplorationStore,
|
||||
SourcesStore: db.SourcesStore,
|
||||
ServersStore: db.ServersStore,
|
||||
UsersStore: db.UsersStore,
|
||||
TimeSeries: &influx.Client{
|
||||
Logger: logger,
|
||||
},
|
||||
LayoutStore: layouts,
|
||||
AlertRulesStore: db.AlertsStore,
|
||||
Logger: logger,
|
||||
UseAuth: useAuth,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,10 @@ type Service struct {
|
|||
ServersStore chronograf.ServersStore
|
||||
LayoutStore chronograf.LayoutStore
|
||||
AlertRulesStore chronograf.AlertRulesStore
|
||||
UsersStore chronograf.UsersStore
|
||||
TimeSeries chronograf.TimeSeries
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
}
|
||||
|
||||
// ErrorMessage is the error response format for all service errors
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
type userLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
Explorations string `json:"explorations"` // URL for explorations endpoint
|
||||
}
|
||||
|
||||
type userResponse struct {
|
||||
*chronograf.User
|
||||
Links userLinks `json:"links"`
|
||||
}
|
||||
|
||||
func newUserResponse(usr *chronograf.User) userResponse {
|
||||
base := "/chronograf/v1/users"
|
||||
return userResponse{
|
||||
User: usr,
|
||||
Links: userLinks{
|
||||
Self: fmt.Sprintf("%s/%d", base, usr.ID),
|
||||
Explorations: fmt.Sprintf("%s/%d/explorations", base, usr.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewUser adds a new valid user to the store
|
||||
func (h *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
||||
var usr *chronograf.User
|
||||
if err := json.NewDecoder(r.Body).Decode(usr); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
if err := ValidUserRequest(usr); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if usr, err = h.UsersStore.Add(r.Context(), usr); err != nil {
|
||||
msg := fmt.Errorf("error storing user %v: %v", *usr, err)
|
||||
unknownErrorWithMessage(w, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(usr)
|
||||
w.Header().Add("Location", res.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, res, h.Logger)
|
||||
}
|
||||
|
||||
// UserID retrieves a user from the store
|
||||
func (h *Service) UserID(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
usr, err := h.UsersStore.Get(ctx, chronograf.UserID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(usr)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
||||
|
||||
// RemoveUser deletes the user from the store
|
||||
func (h *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
usr := &chronograf.User{ID: chronograf.UserID(id)}
|
||||
ctx := r.Context()
|
||||
if err = h.UsersStore.Delete(ctx, usr); err != nil {
|
||||
unknownErrorWithMessage(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateUser handles incremental updates of a data user
|
||||
func (h *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
usr, err := h.UsersStore.Get(ctx, chronograf.UserID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req chronograf.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
usr.Email = req.Email
|
||||
if err := ValidUserRequest(usr); err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.UsersStore.Update(ctx, usr); err != nil {
|
||||
msg := fmt.Sprintf("Error updating user ID %d", id)
|
||||
Error(w, http.StatusInternalServerError, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
encodeJSON(w, http.StatusOK, newUserResponse(usr), h.Logger)
|
||||
}
|
||||
|
||||
// ValidUserRequest checks if email is nonempty
|
||||
func ValidUserRequest(s *chronograf.User) error {
|
||||
// email is required
|
||||
if s.Email == "" {
|
||||
return fmt.Errorf("Email required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEmail(ctx context.Context) (string, error) {
|
||||
principal := ctx.Value(chronograf.PrincipalKey).(chronograf.Principal)
|
||||
if principal == "" {
|
||||
return "", fmt.Errorf("Token not found")
|
||||
}
|
||||
return string(principal), nil
|
||||
}
|
||||
|
||||
// Me does a findOrCreate based on the email in the context
|
||||
func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !h.UseAuth {
|
||||
Error(w, http.StatusTeapot, fmt.Sprintf("%v", "Go to line 151 users.go. Look for Arnold"), h.Logger)
|
||||
_ = 42 // did you mean to learn the answer? if so go to line aslfjasdlfja; (gee willickers.... tbc)
|
||||
return
|
||||
}
|
||||
email, err := getEmail(ctx)
|
||||
if err != nil {
|
||||
invalidData(w, err, h.Logger)
|
||||
return
|
||||
}
|
||||
usr, err := h.UsersStore.FindByEmail(ctx, email)
|
||||
if err == nil {
|
||||
res := newUserResponse(usr)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// Because we didnt find a user, making a new one
|
||||
user := &chronograf.User{
|
||||
Email: email,
|
||||
}
|
||||
user, err = h.UsersStore.Add(ctx, user)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("error storing user %v: %v", user, err)
|
||||
unknownErrorWithMessage(w, msg, h.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(user)
|
||||
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import PermissionsTable from 'src/shared/components/PermissionsTable';
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('Shared.Components.PermissionsTable', function() {
|
||||
it('renders a row for each permission', function() {
|
||||
const permissions = [
|
||||
{name: 'ViewChronograf', displayName: 'View Chronograf', description: 'Can use Chronograf tools', resources: ['db1']},
|
||||
{name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']},
|
||||
];
|
||||
|
||||
const wrapper = shallow(
|
||||
<PermissionsTable
|
||||
permissions={permissions}
|
||||
showAddResource={true}
|
||||
onRemovePermission={sinon.spy()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('tr').length).to.equal(2);
|
||||
expect(wrapper.find('table').text()).to.match(/View Chronograf/);
|
||||
expect(wrapper.find('table').text()).to.match(/db1/);
|
||||
expect(wrapper.find('table').text()).to.match(/Read/);
|
||||
expect(wrapper.find('table').text()).to.match(/All Databases/);
|
||||
});
|
||||
|
||||
it('only renders the control to add a resource when specified', function() {
|
||||
const wrapper = shallow(
|
||||
<PermissionsTable
|
||||
permissions={[{name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']}]}
|
||||
showAddResource={false}
|
||||
onRemovePermission={sinon.spy()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('.pill-add').length).to.equal(0);
|
||||
});
|
||||
|
||||
it('only renders the "Remove" control when a callback is provided', function() {
|
||||
const wrapper = shallow(
|
||||
<PermissionsTable
|
||||
permissions={[{name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']}]}
|
||||
showAddResource={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('.remove-permission').length).to.equal(0);
|
||||
});
|
||||
|
||||
describe('when a user clicks "Remove"', function() {
|
||||
it('fires a callback', function() {
|
||||
const permission = {name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']};
|
||||
const cb = sinon.spy();
|
||||
const wrapper = shallow(
|
||||
<PermissionsTable
|
||||
permissions={[permission]}
|
||||
showAddResource={false}
|
||||
onRemovePermission={cb}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('button[children="Remove"]').at(0).simulate('click');
|
||||
|
||||
expect(cb.calledWith(permission)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
|
@ -53,7 +53,7 @@ const CheckSources = React.createClass({
|
|||
const {isFetching, sources} = nextState;
|
||||
const source = sources.find((s) => s.id === params.sourceID);
|
||||
if (!isFetching && !source) {
|
||||
return router.push(`/?redirectPath=${location.pathname}`);
|
||||
return router.push(`/sources/new?redirectPath=${location.pathname}`);
|
||||
}
|
||||
|
||||
if (!isFetching && !location.pathname.includes("/manage-sources")) {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import {withRouter} from 'react-router';
|
||||
|
||||
const Login = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<a className="btn btn-primary" href="/oauth/github">Click me to log in</a>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default withRouter(Login);
|
|
@ -0,0 +1,2 @@
|
|||
import Login from './Login';
|
||||
export {Login};
|
|
@ -1,25 +1,24 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import {Router, Route, browserHistory} from 'react-router';
|
||||
import {Router, Route, browserHistory, Redirect} from 'react-router';
|
||||
|
||||
import App from 'src/App';
|
||||
import AlertsApp from 'src/alerts';
|
||||
import CheckSources from 'src/CheckSources';
|
||||
import {HostsPage, HostPage} from 'src/hosts';
|
||||
import {KubernetesPage} from 'src/kubernetes';
|
||||
import {Login} from 'src/auth';
|
||||
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor';
|
||||
import DataExplorer from 'src/chronograf';
|
||||
import {CreateSource, SourceForm, ManageSources} from 'src/sources';
|
||||
import NotFound from 'src/shared/components/NotFound';
|
||||
import NoClusterError from 'src/shared/components/NoClusterError';
|
||||
import configureStore from 'src/store/configureStore';
|
||||
import {getSources} from 'shared/apis';
|
||||
import {getMe, getSources} from 'shared/apis';
|
||||
import {receiveMe} from 'shared/actions/me';
|
||||
|
||||
import 'src/style/enterprise_style/application.scss';
|
||||
|
||||
const {number, shape, string, bool} = PropTypes;
|
||||
|
||||
const defaultTimeRange = {upper: null, lower: 'now() - 15m'};
|
||||
const lsTimeRange = window.localStorage.getItem('timeRange');
|
||||
const parsedTimeRange = JSON.parse(lsTimeRange) || {};
|
||||
|
@ -28,38 +27,15 @@ const timeRange = Object.assign(defaultTimeRange, parsedTimeRange);
|
|||
const store = configureStore({timeRange});
|
||||
const rootNode = document.getElementById('react-root');
|
||||
|
||||
const HTTP_SERVER_ERROR = 500;
|
||||
|
||||
const Root = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
me: {
|
||||
id: 1,
|
||||
name: 'Chronograf',
|
||||
email: 'foo@example.com',
|
||||
admin: true,
|
||||
},
|
||||
isFetching: false,
|
||||
hasReadPermission: false,
|
||||
clusterStatus: null,
|
||||
loggedIn: null,
|
||||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
me: shape({
|
||||
id: number.isRequired,
|
||||
name: string.isRequired,
|
||||
email: string.isRequired,
|
||||
admin: bool.isRequired,
|
||||
}),
|
||||
componentDidMount() {
|
||||
this.checkAuth();
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
me: this.state.me,
|
||||
};
|
||||
},
|
||||
|
||||
activeSource(sources) {
|
||||
const defaultSource = sources.find((s) => s.default);
|
||||
if (defaultSource && defaultSource.id) {
|
||||
|
@ -68,29 +44,53 @@ const Root = React.createClass({
|
|||
return sources[0];
|
||||
},
|
||||
|
||||
redirectToHosts(_, replace, callback) {
|
||||
redirectFromRoot(_, replace, callback) {
|
||||
getSources().then(({data: {sources}}) => {
|
||||
if (sources && sources.length) {
|
||||
const path = `/sources/${this.activeSource(sources).id}/hosts`;
|
||||
replace(path);
|
||||
}
|
||||
callback();
|
||||
}).catch(callback);
|
||||
});
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
if (store.getState().me.links) {
|
||||
return this.setState({loggedIn: true});
|
||||
}
|
||||
getMe().then(({data: me}) => {
|
||||
store.dispatch(receiveMe(me));
|
||||
this.setState({loggedIn: true});
|
||||
}).catch((err) => {
|
||||
const ImATeapot = 418;
|
||||
if (err.response.status === ImATeapot) { // This means authentication is not set up!
|
||||
return this.setState({loggedIn: true});
|
||||
// may be good to store this info somewhere. So that pages know whether they can use me or not
|
||||
}
|
||||
|
||||
this.setState({loggedIn: false});
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.state.isFetching) {
|
||||
return null;
|
||||
if (this.state.loggedIn === null) {
|
||||
return <div className="page-spinner"></div>;
|
||||
}
|
||||
|
||||
if (this.state.clusterStatus === HTTP_SERVER_ERROR) {
|
||||
return <NoClusterError />;
|
||||
}
|
||||
|
||||
if (this.state.loggedIn === false) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/" component={CreateSource} onEnter={this.redirectToHosts} />
|
||||
<Route path="/login" component={Login} />
|
||||
<Redirect from="*" to="/login" />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/" component={CreateSource} onEnter={this.redirectFromRoot} />
|
||||
<Route path="/sources/new" component={CreateSource} />
|
||||
<Route path="/sources/:sourceID" component={App}>
|
||||
<Route component={CheckSources}>
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export function receiveMe(me) {
|
||||
return {
|
||||
type: 'ME_RECEIVED',
|
||||
payload: {
|
||||
me,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return {
|
||||
type: 'LOGOUT',
|
||||
};
|
||||
}
|
|
@ -7,6 +7,13 @@ export function fetchLayouts() {
|
|||
});
|
||||
}
|
||||
|
||||
export function getMe() {
|
||||
return AJAX({
|
||||
url: `/chronograf/v1/me`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function getSources() {
|
||||
return AJAX({
|
||||
url: '/chronograf/v1/sources',
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
|
||||
const {arrayOf, number, shape, func, string} = PropTypes;
|
||||
|
||||
const AddClusterAccounts = React.createClass({
|
||||
propTypes: {
|
||||
clusters: arrayOf(shape({
|
||||
id: number.isRequired,
|
||||
cluster_users: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
})),
|
||||
dipslay_name: string,
|
||||
cluster_id: string.isRequired,
|
||||
})).isRequired,
|
||||
onSelectClusterAccount: func.isRequired,
|
||||
headerText: string,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
headerText: 'Pair With Cluster Accounts',
|
||||
};
|
||||
},
|
||||
|
||||
handleSelectClusterAccount(e, clusterID) {
|
||||
this.props.onSelectClusterAccount({
|
||||
clusterID,
|
||||
accountName: e.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.props.clusters.map((cluster, i) => {
|
||||
return (
|
||||
<div key={i} className="form-grid">
|
||||
<div className="form-group col-sm-6">
|
||||
{i === 0 ? <label>Cluster</label> : null}
|
||||
<div className="form-control-static">
|
||||
{cluster.display_name || cluster.cluster_id}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
{i === 0 ? <label>Account</label> : null}
|
||||
{this.renderClusterUsers(cluster)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderClusterUsers(cluster) {
|
||||
if (!cluster.cluster_users) {
|
||||
return (
|
||||
<select disabled={true} defaultValue="No cluster accounts" className="form-control" id="cluster-account">
|
||||
<option>No cluster accounts</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<select onChange={(e) => this.handleSelectClusterAccount(e, cluster.cluster_id)} className="form-control">
|
||||
<option value="">No Association</option>
|
||||
{
|
||||
cluster.cluster_users.map((cu) => {
|
||||
return <option value={cu.name} key={cu.name}>{cu.name}</option>;
|
||||
})
|
||||
}
|
||||
</select>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default AddClusterAccounts;
|
|
@ -1,124 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
|
||||
const CLUSTER_WIDE_PERMISSIONS = ["CreateDatabase", "AddRemoveNode", "ManageShard", "DropDatabase", "CopyShard", "Rebalance"];
|
||||
|
||||
const AddPermissionModal = React.createClass({
|
||||
propTypes: {
|
||||
activeCluster: PropTypes.string.isRequired,
|
||||
permissions: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
})),
|
||||
databases: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
onAddPermission: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedPermission: null,
|
||||
selectedDatabase: '',
|
||||
};
|
||||
},
|
||||
|
||||
handlePermissionClick(permission) {
|
||||
this.setState({
|
||||
selectedPermission: permission,
|
||||
selectedDatabase: '',
|
||||
});
|
||||
},
|
||||
|
||||
handleDatabaseChange(e) {
|
||||
this.setState({selectedDatabase: e.target.value});
|
||||
},
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.onAddPermission({
|
||||
name: this.state.selectedPermission,
|
||||
resources: [this.state.selectedDatabase],
|
||||
});
|
||||
$('#addPermissionModal').modal('hide'); // eslint-disable-line no-undef
|
||||
},
|
||||
|
||||
render() {
|
||||
const {permissions} = this.props;
|
||||
|
||||
return (
|
||||
<div className="modal fade" id="addPermissionModal" tabIndex="-1" role="dialog">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 className="modal-title">Select a Permission to Add</h4>
|
||||
</div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="well permission-list">
|
||||
<ul>
|
||||
{permissions.map((perm) => {
|
||||
return (
|
||||
<li key={perm.name}>
|
||||
<input onClick={() => this.handlePermissionClick(perm.name)} type="radio" name="permissionName" value={`${perm.name}`} id={`permission-${perm.name}`}></input>
|
||||
<label htmlFor={`permission-${perm.name}`}>
|
||||
{perm.displayName}
|
||||
<br/>
|
||||
<span className="permission-description">{perm.description}</span>
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{this.renderOptions()}
|
||||
</div>
|
||||
{this.renderFooter()}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderFooter() {
|
||||
return (
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<input disabled={!this.state.selectedPermission} className="btn btn-success" type="submit" value="Add Permission"></input>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderOptions() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.selectedPermission ? this.renderDatabases() : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderDatabases() {
|
||||
const isClusterWide = CLUSTER_WIDE_PERMISSIONS.includes(this.state.selectedPermission);
|
||||
if (!this.props.databases.length || isClusterWide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="form-grid">
|
||||
<div className="form-group col-md-12">
|
||||
<label htmlFor="#permissions-database">Limit Permission to...</label>
|
||||
<select onChange={this.handleDatabaseChange} className="form-control" name="database" id="permissions-database">
|
||||
<option value={''}>All Databases</option>
|
||||
{this.props.databases.map((databaseName, i) => <option key={i}>{databaseName}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default AddPermissionModal;
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const {node} = React.PropTypes;
|
||||
const ClusterError = React.createClass({
|
||||
propTypes: {
|
||||
children: node.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-sm-offset-3">
|
||||
<div className="panel panel-error panel-summer">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default ClusterError;
|
|
@ -1,21 +0,0 @@
|
|||
import React from 'react';
|
||||
import ClusterError from './ClusterError';
|
||||
|
||||
const InsufficientPermissions = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<ClusterError>
|
||||
<div className="panel-heading text-center">
|
||||
<h2 className="deluxe">
|
||||
{`Your account has insufficient permissions`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="panel-body text-center">
|
||||
<h3 className="deluxe">Talk to your admin to get additional permissions for access</h3>
|
||||
</div>
|
||||
</ClusterError>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default InsufficientPermissions;
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react';
|
||||
import errorCopy from 'hson!shared/copy/errors.hson';
|
||||
|
||||
const NoClusterError = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-sm-offset-3">
|
||||
<div className="panel panel-error panel-summer">
|
||||
<div className="panel-heading text-center">
|
||||
<h2 className="deluxe">
|
||||
{errorCopy.noCluster.head}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="panel-body text-center">
|
||||
<h3 className="deluxe">How to resolve:</h3>
|
||||
<p>
|
||||
{errorCopy.noCluster.body}
|
||||
</p>
|
||||
<div className="text-center">
|
||||
<button className="btn btn-center btn-success" onClick={() => window.location.reload()}>My Cluster Is Back Up</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default NoClusterError;
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const NoClusterLinksError = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-sm-offset-3">
|
||||
<div className="panel panel-error panel-summer">
|
||||
<div className="panel-heading text-center">
|
||||
<h2 className="deluxe">
|
||||
This user is not associated with any cluster accounts!
|
||||
</h2>
|
||||
</div>
|
||||
<div className="panel-body text-center">
|
||||
<p>Many features in Chronograf require your user to be associated with a cluster account.</p>
|
||||
<p>Ask an administrator to associate your user with a cluster account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default NoClusterLinksError;
|
|
@ -1,76 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
|
||||
const {arrayOf, shape, string} = PropTypes;
|
||||
|
||||
const PermissionsTable = React.createClass({
|
||||
propTypes: {
|
||||
permissions: PropTypes.arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
displayName: string.isRequired,
|
||||
description: string.isRequired,
|
||||
resources: arrayOf(string.isRequired).isRequired,
|
||||
})).isRequired,
|
||||
showAddResource: PropTypes.bool,
|
||||
onRemovePermission: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
permissions: [],
|
||||
showAddResource: false,
|
||||
};
|
||||
},
|
||||
|
||||
handleAddResourceClick() {
|
||||
// TODO
|
||||
},
|
||||
|
||||
handleRemovePermission(permission) {
|
||||
this.props.onRemovePermission(permission);
|
||||
},
|
||||
|
||||
render() {
|
||||
if (!this.props.permissions.length) {
|
||||
return (
|
||||
<div className="generic-empty-state">
|
||||
<span className="icon alert-triangle"></span>
|
||||
<h4>This Role has no Permissions</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel-body">
|
||||
<table className="table permissions-table">
|
||||
<tbody>
|
||||
{this.props.permissions.map((p) => (
|
||||
<tr key={p.name}>
|
||||
<td>{p.displayName}</td>
|
||||
<td>
|
||||
{p.resources.map((resource, i) => <div key={i} className="pill">{resource === '' ? 'All Databases' : resource}</div>)}
|
||||
{this.props.showAddResource ? (
|
||||
<div onClick={this.handleAddResourceClick} className="pill-add" data-toggle="modal" data-target="#addPermissionModal">
|
||||
<span className="icon plus"></span>
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
{this.props.onRemovePermission ? (
|
||||
<td className="remove-permission">
|
||||
<button
|
||||
onClick={() => this.handleRemovePermission(p)}
|
||||
type="button"
|
||||
className="btn btn-sm btn-link-danger">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default PermissionsTable;
|
|
@ -1,86 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import PermissionsTable from 'src/shared/components/PermissionsTable';
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes;
|
||||
|
||||
const RolePanels = React.createClass({
|
||||
propTypes: {
|
||||
roles: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
users: arrayOf(string.isRequired).isRequired,
|
||||
permissions: arrayOf(shape({
|
||||
name: string.isRequired,
|
||||
displayName: string.isRequired,
|
||||
description: string.isRequired,
|
||||
resources: arrayOf(string.isRequired).isRequired,
|
||||
})).isRequired,
|
||||
})).isRequired,
|
||||
showUserCount: bool,
|
||||
onRemoveAccountFromRole: func,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
showUserCount: false,
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
const {roles} = this.props;
|
||||
|
||||
if (!roles.length) {
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<div className="generic-empty-state">
|
||||
<span className="icon alert-triangle"></span>
|
||||
<h4>This user has no roles</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel-group sub-page" role="tablist">
|
||||
{roles.map((role) => {
|
||||
const id = role.name.replace(/[^\w]/gi, '');
|
||||
return (
|
||||
<div key={role.name} className="panel panel-default">
|
||||
<div className="panel-heading" role="tab" id={`heading${id}`}>
|
||||
<h4 className="panel-title u-flex u-ai-center u-jc-space-between">
|
||||
<a className="collapsed" role="button" data-toggle="collapse" href={`#collapse-role-${id}`}>
|
||||
<span className="caret"></span>
|
||||
{role.name}
|
||||
</a>
|
||||
<div>
|
||||
{this.props.showUserCount ? <p>{role.users ? role.users.length : 0} Users</p> : null}
|
||||
{this.props.onRemoveAccountFromRole ? (
|
||||
<button
|
||||
onClick={() => this.props.onRemoveAccountFromRole(role)}
|
||||
data-toggle="modal"
|
||||
data-target="#removeAccountFromRoleModal"
|
||||
type="button"
|
||||
className="btn btn-sm btn-link">
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
<Link to={`/roles/${encodeURIComponent(role.name)}`} className="btn btn-xs btn-link">
|
||||
Go To Role
|
||||
</Link>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div id={`collapse-role-${id}`} className="panel-collapse collapse" role="tabpanel">
|
||||
<PermissionsTable permissions={role.permissions} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default RolePanels;
|
|
@ -1,95 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const {func, shape, arrayOf, string} = PropTypes;
|
||||
const UsersTable = React.createClass({
|
||||
propTypes: {
|
||||
users: arrayOf(shape({}).isRequired).isRequired,
|
||||
activeCluster: string.isRequired,
|
||||
onUserToDelete: func.isRequired,
|
||||
me: shape({}).isRequired,
|
||||
deleteText: string,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
deleteText: 'Delete',
|
||||
};
|
||||
},
|
||||
|
||||
handleSelectUserToDelete(user) {
|
||||
this.props.onUserToDelete(user);
|
||||
},
|
||||
render() {
|
||||
const {users, activeCluster, me} = this.props;
|
||||
|
||||
if (!users.length) {
|
||||
return (
|
||||
<div className="generic-empty-state">
|
||||
<span className="icon user-outline"/>
|
||||
<h4>No users</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table v-center users-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Admin</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{
|
||||
users.map((user) => {
|
||||
const isMe = me.id === user.id;
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
<td></td>
|
||||
<td>
|
||||
<span>
|
||||
<Link to={`/clusters/${activeCluster}/users/${user.id}`} title={`Go to ${user.name}'s profile`}>{user.name}</Link>
|
||||
{isMe ? <em> (You) </em> : null}
|
||||
</span>
|
||||
</td>
|
||||
<td className="admin-column">{this.renderAdminIcon(user.admin)}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
{this.renderDeleteButton(user)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
renderAdminIcon(isAdmin) {
|
||||
return <span className={classNames("icon", {"checkmark text-color-success": isAdmin, "remove text-color-danger": !isAdmin})}></span>;
|
||||
},
|
||||
|
||||
renderDeleteButton(user) {
|
||||
if (this.props.me.id === user.id) {
|
||||
return <button type="button" className="btn btn-sm btn-link-danger disabled" title={`Cannot ${this.props.deleteText} Yourself`}>{this.props.deleteText}</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => this.handleSelectUserToDelete({id: user.id, name: user.name})}
|
||||
type="button"
|
||||
data-toggle="modal"
|
||||
data-target="#deleteUsersModal"
|
||||
className="btn btn-sm btn-link-danger"
|
||||
>
|
||||
{this.props.deleteText}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default UsersTable;
|
|
@ -0,0 +1,7 @@
|
|||
import me from './me';
|
||||
import notifications from './notifications';
|
||||
|
||||
export {
|
||||
me,
|
||||
notifications,
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
function getInitialState() {
|
||||
return {};
|
||||
}
|
||||
const initialState = getInitialState();
|
||||
|
||||
export default function me(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'ME_RECEIVED': {
|
||||
return action.payload.me;
|
||||
}
|
||||
case 'LOGOUT': {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -3,11 +3,15 @@ import {combineReducers} from 'redux';
|
|||
import thunkMiddleware from 'redux-thunk';
|
||||
import makeQueryExecuter from 'src/shared/middleware/queryExecuter';
|
||||
import * as chronografReducers from 'src/chronograf/reducers';
|
||||
import * as sharedReducers from 'src/shared/reducers';
|
||||
import rulesReducer from 'src/kapacitor/reducers/rules';
|
||||
import notifications from 'src/shared/reducers/notifications';
|
||||
import persistStateEnhancer from './persistStateEnhancer';
|
||||
|
||||
const rootReducer = combineReducers({notifications, ...chronografReducers, rules: rulesReducer});
|
||||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
...chronografReducers,
|
||||
rules: rulesReducer,
|
||||
});
|
||||
|
||||
export default function configureStore(initialState) {
|
||||
const createPersistentStore = compose(
|
||||
|
|
Loading…
Reference in New Issue