commit
01ec21b483
|
@ -325,6 +325,36 @@ type UsersStore interface {
|
||||||
Update(context.Context, *User) error
|
Update(context.Context, *User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Database represents a database in a time series source
|
||||||
|
type Database struct {
|
||||||
|
Name string `json:"name"` // a unique string identifier for the database
|
||||||
|
Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy)
|
||||||
|
Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy)
|
||||||
|
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetentionPolicy represents a retention policy in a time series source
|
||||||
|
type RetentionPolicy struct {
|
||||||
|
Name string `json:"name"` // a unique string identifier for the retention policy
|
||||||
|
Duration string `json:"duration,omitempty"` // the duration
|
||||||
|
Replication int32 `json:"replication,omitempty"` // the replication factor
|
||||||
|
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration
|
||||||
|
Default bool `json:"isDefault,omitempty"` // whether the RP should be the default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Databases represents a databases in a time series source
|
||||||
|
type Databases interface {
|
||||||
|
// All lists all databases
|
||||||
|
AllDB(context.Context) ([]Database, error)
|
||||||
|
Connect(context.Context, *Source) error
|
||||||
|
CreateDB(context.Context, *Database) (*Database, error)
|
||||||
|
DropDB(context.Context, string) error
|
||||||
|
AllRP(context.Context, string) ([]RetentionPolicy, error)
|
||||||
|
CreateRP(context.Context, string, *RetentionPolicy) (*RetentionPolicy, error)
|
||||||
|
UpdateRP(context.Context, string, string, *RetentionPolicy) (*RetentionPolicy, error)
|
||||||
|
DropRP(context.Context, string, string) error
|
||||||
|
}
|
||||||
|
|
||||||
// DashboardID is the dashboard ID
|
// DashboardID is the dashboard ID
|
||||||
type DashboardID int
|
type DashboardID int
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
package influx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllDB returns all databases from within Influx
|
||||||
|
func (c *Client) AllDB(ctx context.Context) ([]chronograf.Database, error) {
|
||||||
|
databases, err := c.showDatabases(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return databases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDB creates a database within Influx
|
||||||
|
func (c *Client) CreateDB(ctx context.Context, db *chronograf.Database) (*chronograf.Database, error) {
|
||||||
|
_, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: fmt.Sprintf(`CREATE DATABASE "%s"`, db.Name),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &chronograf.Database{Name: db.Name}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropDB drops a database within Influx
|
||||||
|
func (c *Client) DropDB(ctx context.Context, database string) error {
|
||||||
|
_, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: fmt.Sprintf(`DROP DATABASE "%s"`, database),
|
||||||
|
DB: database,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllRP returns all the retention policies for a specific database
|
||||||
|
func (c *Client) AllRP(ctx context.Context, database string) ([]chronograf.RetentionPolicy, error) {
|
||||||
|
retentionPolicies, err := c.showRetentionPolicies(ctx, database)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return retentionPolicies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getRP(ctx context.Context, db, name string) (chronograf.RetentionPolicy, error) {
|
||||||
|
rps, err := c.AllRP(ctx, db)
|
||||||
|
if err != nil {
|
||||||
|
return chronograf.RetentionPolicy{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rp := range rps {
|
||||||
|
if rp.Name == name {
|
||||||
|
return rp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chronograf.RetentionPolicy{}, fmt.Errorf("unknown retention policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRP creates a retention policy for a specific database
|
||||||
|
func (c *Client) CreateRP(ctx context.Context, database string, rp *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) {
|
||||||
|
query := fmt.Sprintf(`CREATE RETENTION POLICY "%s" ON "%s" DURATION %s REPLICATION %d`, rp.Name, database, rp.Duration, rp.Replication)
|
||||||
|
if len(rp.ShardDuration) != 0 {
|
||||||
|
query = fmt.Sprintf(`%s SHARD DURATION %s`, query, rp.ShardDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rp.Default {
|
||||||
|
query = fmt.Sprintf(`%s DEFAULT`, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: query,
|
||||||
|
DB: database,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.getRP(ctx, database, rp.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRP updates a specific retention policy for a specific database
|
||||||
|
func (c *Client) UpdateRP(ctx context.Context, database string, name string, rp *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteString(fmt.Sprintf(`ALTER RETENTION POLICY "%s" ON "%s"`, name, database))
|
||||||
|
if len(rp.Duration) > 0 {
|
||||||
|
buffer.WriteString(" DURATION " + rp.Duration)
|
||||||
|
}
|
||||||
|
if rp.Replication > 0 {
|
||||||
|
buffer.WriteString(" REPLICATION " + fmt.Sprint(rp.Replication))
|
||||||
|
}
|
||||||
|
if len(rp.ShardDuration) > 0 {
|
||||||
|
buffer.WriteString(" SHARD DURATION " + rp.ShardDuration)
|
||||||
|
}
|
||||||
|
if rp.Default == true {
|
||||||
|
buffer.WriteString(" DEFAULT")
|
||||||
|
}
|
||||||
|
queryRes, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: buffer.String(),
|
||||||
|
DB: database,
|
||||||
|
RP: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ALTER RETENTION POLICIES statements puts the error within the results itself
|
||||||
|
// So, we have to crack open the results to see what happens
|
||||||
|
octets, err := queryRes.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]struct{ Error string }, 0)
|
||||||
|
if err := json.Unmarshal(octets, &results); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// At last, we can check if there are any error strings
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Error != "" {
|
||||||
|
return nil, fmt.Errorf(r.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.getRP(ctx, database, rp.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropRP removes a specific retention policy for a specific database
|
||||||
|
func (c *Client) DropRP(ctx context.Context, database string, rp string) error {
|
||||||
|
_, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: fmt.Sprintf(`DROP RETENTION POLICY "%s" ON "%s"`, rp, database),
|
||||||
|
DB: database,
|
||||||
|
RP: rp,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) showDatabases(ctx context.Context) ([]chronograf.Database, error) {
|
||||||
|
res, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: `SHOW DATABASES`,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
octets, err := res.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := showResults{}
|
||||||
|
if err := json.Unmarshal(octets, &results); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.Databases(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) showRetentionPolicies(ctx context.Context, name string) ([]chronograf.RetentionPolicy, error) {
|
||||||
|
retentionPolicies, err := c.Query(ctx, chronograf.Query{
|
||||||
|
Command: fmt.Sprintf(`SHOW RETENTION POLICIES ON "%s"`, name),
|
||||||
|
DB: name,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
octets, err := retentionPolicies.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := showResults{}
|
||||||
|
if err := json.Unmarshal(octets, &results); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.RetentionPolicies(), nil
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
var _ chronograf.TimeSeries = &Client{}
|
var _ chronograf.TimeSeries = &Client{}
|
||||||
var _ chronograf.TSDBStatus = &Client{}
|
var _ chronograf.TSDBStatus = &Client{}
|
||||||
|
var _ chronograf.Databases = &Client{}
|
||||||
|
|
||||||
// Shared transports for all clients to prevent leaking connections
|
// Shared transports for all clients to prevent leaking connections
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -75,6 +75,55 @@ func (r *showResults) Users() []chronograf.User {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Databases converts SHOW DATABASES to chronograf Databases
|
||||||
|
func (r *showResults) Databases() []chronograf.Database {
|
||||||
|
res := []chronograf.Database{}
|
||||||
|
for _, u := range *r {
|
||||||
|
for _, s := range u.Series {
|
||||||
|
for _, v := range s.Values {
|
||||||
|
if name, ok := v[0].(string); !ok {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
d := chronograf.Database{Name: name}
|
||||||
|
res = append(res, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *showResults) RetentionPolicies() []chronograf.RetentionPolicy {
|
||||||
|
res := []chronograf.RetentionPolicy{}
|
||||||
|
for _, u := range *r {
|
||||||
|
for _, s := range u.Series {
|
||||||
|
for _, v := range s.Values {
|
||||||
|
if name, ok := v[0].(string); !ok {
|
||||||
|
continue
|
||||||
|
} else if duration, ok := v[1].(string); !ok {
|
||||||
|
continue
|
||||||
|
} else if sduration, ok := v[2].(string); !ok {
|
||||||
|
continue
|
||||||
|
} else if replication, ok := v[3].(float64); !ok {
|
||||||
|
continue
|
||||||
|
} else if def, ok := v[4].(bool); !ok {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
d := chronograf.RetentionPolicy{
|
||||||
|
Name: name,
|
||||||
|
Duration: duration,
|
||||||
|
ShardDuration: sduration,
|
||||||
|
Replication: int32(replication),
|
||||||
|
Default: def,
|
||||||
|
}
|
||||||
|
res = append(res, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Permissions converts SHOW GRANTS to chronograf.Permissions
|
// Permissions converts SHOW GRANTS to chronograf.Permissions
|
||||||
func (r *showResults) Permissions() chronograf.Permissions {
|
func (r *showResults) Permissions() chronograf.Permissions {
|
||||||
res := []chronograf.Permission{}
|
res := []chronograf.Permission{}
|
||||||
|
|
|
@ -4,9 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/bouk/httprouter"
|
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
"github.com/influxdata/chronograf/influx"
|
"github.com/influxdata/chronograf/influx"
|
||||||
"github.com/influxdata/chronograf/uuid"
|
"github.com/influxdata/chronograf/uuid"
|
||||||
|
@ -160,7 +158,7 @@ func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
// ReplaceDashboard completely replaces a dashboard
|
// ReplaceDashboard completely replaces a dashboard
|
||||||
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
|
idParam, err := paramID("id", r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
||||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
@ -198,10 +196,11 @@ func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
// UpdateDashboard completely updates either the dashboard name or the cells
|
// UpdateDashboard completely updates either the dashboard name or the cells
|
||||||
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
|
idParam, err := paramID("id", r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
||||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
id := chronograf.DashboardID(idParam)
|
id := chronograf.DashboardID(idParam)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,411 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bouk/httprouter"
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbLinks struct {
|
||||||
|
Self string `json:"self"` // Self link mapping to this resource
|
||||||
|
RPs string `json:"retentionPolicies"` // URL for retention policies for this database
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbResponse struct {
|
||||||
|
Name string `json:"name"` // a unique string identifier for the database
|
||||||
|
Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy)
|
||||||
|
Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy)
|
||||||
|
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy)
|
||||||
|
RPs []rpResponse `json:"retentionPolicies,omitempty"` // RPs are the retention policies for a database
|
||||||
|
Links dbLinks `json:"links"` // Links are URI locations related to the database
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDBResponse creates the response for the /databases endpoint
|
||||||
|
func newDBResponse(srcID int, name string, rps []rpResponse) dbResponse {
|
||||||
|
base := "/chronograf/v1/sources"
|
||||||
|
return dbResponse{
|
||||||
|
Name: name,
|
||||||
|
RPs: rps,
|
||||||
|
Links: dbLinks{
|
||||||
|
Self: fmt.Sprintf("%s/%d/dbs/%s", base, srcID, name),
|
||||||
|
RPs: fmt.Sprintf("%s/%d/dbs/%s/rps", base, srcID, name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbsResponse struct {
|
||||||
|
Databases []dbResponse `json:"databases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpLinks struct {
|
||||||
|
Self string `json:"self"` // Self link mapping to this resource
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpResponse struct {
|
||||||
|
Name string `json:"name"` // a unique string identifier for the retention policy
|
||||||
|
Duration string `json:"duration"` // the duration
|
||||||
|
Replication int32 `json:"replication"` // the replication factor
|
||||||
|
ShardDuration string `json:"shardDuration"` // the shard duration
|
||||||
|
Default bool `json:"isDefault"` // whether the RP should be the default
|
||||||
|
Links rpLinks `json:"links"` // Links are URI locations related to the database
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLinks adds links to an rpResponse in place
|
||||||
|
func (r *rpResponse) WithLinks(srcID int, dbName string) {
|
||||||
|
base := "/chronograf/v1/sources"
|
||||||
|
r.Links = rpLinks{
|
||||||
|
Self: fmt.Sprintf("%s/%d/dbs/%s/rps/%s", base, srcID, dbName, r.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpsResponse struct {
|
||||||
|
RetentionPolicies []rpResponse `json:"retentionPolicies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatabases queries the list of all databases for a source
|
||||||
|
func (h *Service) GetDatabases(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
databases, err := db.AllDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbs := make([]dbResponse, len(databases))
|
||||||
|
for i, d := range databases {
|
||||||
|
rps, err := h.allRPs(ctx, db, srcID, d.Name)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dbs[i] = newDBResponse(srcID, d.Name, rps)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := dbsResponse{
|
||||||
|
Databases: dbs,
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase creates a new database within the datastore
|
||||||
|
func (h *Service) NewDatabase(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
postedDB := &chronograf.Database{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(postedDB); err != nil {
|
||||||
|
invalidJSON(w, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidDatabaseRequest(postedDB); err != nil {
|
||||||
|
invalidData(w, err, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
database, err := db.CreateDB(ctx, postedDB)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rps, err := h.allRPs(ctx, db, srcID, database.Name)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := newDBResponse(srcID, database.Name, rps)
|
||||||
|
encodeJSON(w, http.StatusCreated, res, h.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropDatabase removes a database from a data source
|
||||||
|
func (h *Service) DropDatabase(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbID := httprouter.GetParamFromContext(ctx, "dbid")
|
||||||
|
|
||||||
|
dropErr := db.DropDB(ctx, dbID)
|
||||||
|
if dropErr != nil {
|
||||||
|
Error(w, http.StatusBadRequest, dropErr.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetentionPolicies lists retention policies within a database
|
||||||
|
func (h *Service) RetentionPolicies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbID := httprouter.GetParamFromContext(ctx, "dbid")
|
||||||
|
res, err := h.allRPs(ctx, db, srcID, dbID)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect get RPs %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encodeJSON(w, http.StatusOK, res, h.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Service) allRPs(ctx context.Context, db chronograf.Databases, srcID int, dbID string) ([]rpResponse, error) {
|
||||||
|
allRP, err := db.AllRP(ctx, dbID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rps := make([]rpResponse, len(allRP))
|
||||||
|
for i, rp := range allRP {
|
||||||
|
rp := rpResponse{
|
||||||
|
Name: rp.Name,
|
||||||
|
Duration: rp.Duration,
|
||||||
|
Replication: rp.Replication,
|
||||||
|
ShardDuration: rp.ShardDuration,
|
||||||
|
Default: rp.Default,
|
||||||
|
}
|
||||||
|
rp.WithLinks(srcID, dbID)
|
||||||
|
rps[i] = rp
|
||||||
|
}
|
||||||
|
return rps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRetentionPolicy creates a new retention policy for a database
|
||||||
|
func (h *Service) NewRetentionPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
postedRP := &chronograf.RetentionPolicy{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(postedRP); err != nil {
|
||||||
|
invalidJSON(w, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ValidRetentionPolicyRequest(postedRP); err != nil {
|
||||||
|
invalidData(w, err, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbID := httprouter.GetParamFromContext(ctx, "dbid")
|
||||||
|
rp, err := db.CreateRP(ctx, dbID, postedRP)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := rpResponse{
|
||||||
|
Name: rp.Name,
|
||||||
|
Duration: rp.Duration,
|
||||||
|
Replication: rp.Replication,
|
||||||
|
ShardDuration: rp.ShardDuration,
|
||||||
|
Default: rp.Default,
|
||||||
|
}
|
||||||
|
res.WithLinks(srcID, dbID)
|
||||||
|
encodeJSON(w, http.StatusCreated, res, h.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRetentionPolicy modifies an existing retention policy for a database
|
||||||
|
func (h *Service) UpdateRetentionPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
postedRP := &chronograf.RetentionPolicy{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(postedRP); err != nil {
|
||||||
|
invalidJSON(w, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ValidRetentionPolicyRequest(postedRP); err != nil {
|
||||||
|
invalidData(w, err, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbID := httprouter.GetParamFromContext(ctx, "dbid")
|
||||||
|
rpID := httprouter.GetParamFromContext(ctx, "rpid")
|
||||||
|
rp, err := db.UpdateRP(ctx, dbID, rpID, postedRP)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := rpResponse{
|
||||||
|
Name: rp.Name,
|
||||||
|
Duration: rp.Duration,
|
||||||
|
Replication: rp.Replication,
|
||||||
|
ShardDuration: rp.ShardDuration,
|
||||||
|
Default: rp.Default,
|
||||||
|
}
|
||||||
|
res.WithLinks(srcID, dbID)
|
||||||
|
encodeJSON(w, http.StatusCreated, res, h.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropRetentionPolicy removes a retention policy from a database
|
||||||
|
func (h *Service) DropRetentionPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
srcID, err := paramID("id", r)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := h.SourcesStore.Get(ctx, srcID)
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, srcID, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := h.Databases
|
||||||
|
if err = db.Connect(ctx, &src); err != nil {
|
||||||
|
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
|
||||||
|
Error(w, http.StatusBadRequest, msg, h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbID := httprouter.GetParamFromContext(ctx, "dbid")
|
||||||
|
rpID := httprouter.GetParamFromContext(ctx, "rpid")
|
||||||
|
dropErr := db.DropRP(ctx, dbID, rpID)
|
||||||
|
if dropErr != nil {
|
||||||
|
Error(w, http.StatusBadRequest, dropErr.Error(), h.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidDatabaseRequest checks if the database posted is valid
|
||||||
|
func ValidDatabaseRequest(d *chronograf.Database) error {
|
||||||
|
if len(d.Name) == 0 {
|
||||||
|
return fmt.Errorf("name is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidRetentionPolicyRequest checks if a retention policy is valid on POST
|
||||||
|
func ValidRetentionPolicyRequest(rp *chronograf.RetentionPolicy) error {
|
||||||
|
if len(rp.Name) == 0 {
|
||||||
|
return fmt.Errorf("name is required")
|
||||||
|
}
|
||||||
|
if len(rp.Duration) == 0 {
|
||||||
|
return fmt.Errorf("duration is required")
|
||||||
|
}
|
||||||
|
if rp.Replication == 0 {
|
||||||
|
return fmt.Errorf("replication factor is invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,349 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_GetDatabases(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.GetDatabases(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_NewDatabase(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.NewDatabase(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_DropDatabase(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.DropDatabase(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_RetentionPolicies(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.RetentionPolicies(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_NewRetentionPolicy(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.NewRetentionPolicy(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_UpdateRetentionPolicy(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.UpdateRetentionPolicy(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_DropRetentionPolicy(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
SourcesStore chronograf.SourcesStore
|
||||||
|
ServersStore chronograf.ServersStore
|
||||||
|
LayoutStore chronograf.LayoutStore
|
||||||
|
AlertRulesStore chronograf.AlertRulesStore
|
||||||
|
UsersStore chronograf.UsersStore
|
||||||
|
DashboardsStore chronograf.DashboardsStore
|
||||||
|
TimeSeriesClient TimeSeriesClient
|
||||||
|
Logger chronograf.Logger
|
||||||
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := &Service{
|
||||||
|
SourcesStore: tt.fields.SourcesStore,
|
||||||
|
ServersStore: tt.fields.ServersStore,
|
||||||
|
LayoutStore: tt.fields.LayoutStore,
|
||||||
|
AlertRulesStore: tt.fields.AlertRulesStore,
|
||||||
|
UsersStore: tt.fields.UsersStore,
|
||||||
|
DashboardsStore: tt.fields.DashboardsStore,
|
||||||
|
TimeSeriesClient: tt.fields.TimeSeriesClient,
|
||||||
|
Logger: tt.fields.Logger,
|
||||||
|
UseAuth: tt.fields.UseAuth,
|
||||||
|
Databases: tt.fields.Databases,
|
||||||
|
}
|
||||||
|
h.DropRetentionPolicy(tt.args.w, tt.args.r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidDatabaseRequest(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
d *chronograf.Database
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := ValidDatabaseRequest(tt.args.d); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValidDatabaseRequest() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidRetentionPolicyRequest(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
rp *chronograf.RetentionPolicy
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := ValidRetentionPolicyRequest(tt.args.rp); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValidRetentionPolicyRequest() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,6 +138,19 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
|
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
|
||||||
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
|
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
|
||||||
|
|
||||||
|
// Databases
|
||||||
|
router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases)
|
||||||
|
router.POST("/chronograf/v1/sources/:id/dbs", service.NewDatabase)
|
||||||
|
|
||||||
|
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid", service.DropDatabase)
|
||||||
|
|
||||||
|
// Retention Policies
|
||||||
|
router.GET("/chronograf/v1/sources/:id/dbs/:dbid/rps", service.RetentionPolicies)
|
||||||
|
router.POST("/chronograf/v1/sources/:id/dbs/:dbid/rps", service.NewRetentionPolicy)
|
||||||
|
|
||||||
|
router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.UpdateRetentionPolicy)
|
||||||
|
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.DropRetentionPolicy)
|
||||||
|
|
||||||
var authRoutes AuthRoutes
|
var authRoutes AuthRoutes
|
||||||
|
|
||||||
var out http.Handler
|
var out http.Handler
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
client "github.com/influxdata/usage-client/v1"
|
client "github.com/influxdata/usage-client/v1"
|
||||||
flags "github.com/jessevdk/go-flags"
|
flags "github.com/jessevdk/go-flags"
|
||||||
"github.com/tylerb/graceful"
|
"github.com/tylerb/graceful"
|
||||||
|
"github.com/influxdata/chronograf/influx"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -293,6 +294,7 @@ func openService(ctx context.Context, boltPath, cannedPath string, logger chrono
|
||||||
AlertRulesStore: db.AlertsStore,
|
AlertRulesStore: db.AlertsStore,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
UseAuth: useAuth,
|
UseAuth: useAuth,
|
||||||
|
Databases: &influx.Client{Logger: logger},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Service struct {
|
||||||
TimeSeriesClient TimeSeriesClient
|
TimeSeriesClient TimeSeriesClient
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
UseAuth bool
|
UseAuth bool
|
||||||
|
Databases chronograf.Databases
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeSeriesClient returns the correct client for a time series database.
|
// TimeSeriesClient returns the correct client for a time series database.
|
||||||
|
|
|
@ -18,6 +18,7 @@ type sourceLinks struct {
|
||||||
Permissions string `json:"permissions"` // URL for all allowed permissions for this source
|
Permissions string `json:"permissions"` // URL for all allowed permissions for this source
|
||||||
Users string `json:"users"` // URL for all users associated with this source
|
Users string `json:"users"` // URL for all users associated with this source
|
||||||
Roles string `json:"roles,omitempty"` // URL for all users associated with this source
|
Roles string `json:"roles,omitempty"` // URL for all users associated with this source
|
||||||
|
Databases string `json:"databases"` // URL for the databases contained within this soure
|
||||||
}
|
}
|
||||||
|
|
||||||
type sourceResponse struct {
|
type sourceResponse struct {
|
||||||
|
@ -43,6 +44,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
|
||||||
Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID),
|
Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID),
|
||||||
Permissions: fmt.Sprintf("%s/%d/permissions", httpAPISrcs, src.ID),
|
Permissions: fmt.Sprintf("%s/%d/permissions", httpAPISrcs, src.ID),
|
||||||
Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID),
|
Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID),
|
||||||
|
Databases: fmt.Sprintf("%s/%d/dbs", httpAPISrcs, src.ID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ func Test_newSourceResponse(t *testing.T) {
|
||||||
Kapacitors: "/chronograf/v1/sources/1/kapacitors",
|
Kapacitors: "/chronograf/v1/sources/1/kapacitors",
|
||||||
Users: "/chronograf/v1/sources/1/users",
|
Users: "/chronograf/v1/sources/1/users",
|
||||||
Permissions: "/chronograf/v1/sources/1/permissions",
|
Permissions: "/chronograf/v1/sources/1/permissions",
|
||||||
|
Databases: "/chronograf/v1/sources/1/dbs",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -50,6 +51,7 @@ func Test_newSourceResponse(t *testing.T) {
|
||||||
Kapacitors: "/chronograf/v1/sources/1/kapacitors",
|
Kapacitors: "/chronograf/v1/sources/1/kapacitors",
|
||||||
Users: "/chronograf/v1/sources/1/users",
|
Users: "/chronograf/v1/sources/1/users",
|
||||||
Permissions: "/chronograf/v1/sources/1/permissions",
|
Permissions: "/chronograf/v1/sources/1/permissions",
|
||||||
|
Databases: "/chronograf/v1/sources/1/dbs",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -769,6 +769,329 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/sources/{id}/dbs/": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"databases"
|
||||||
|
],
|
||||||
|
"summary": "Retrieve all databases for a source",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Listing of all databases for a source",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Databases"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Data source id does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"databases"
|
||||||
|
],
|
||||||
|
"summary": "Create new database for a source",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "database",
|
||||||
|
"in": "body",
|
||||||
|
"description": "Configuration options for a database",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Database"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Database successfully created.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Database"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Data source id does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/sources/{id}/dbs/{db_id}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"databases"
|
||||||
|
],
|
||||||
|
"summary": "Delete database for a source",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "db_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the database",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Database has been deleted",
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Data source id does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/sources/{id}/dbs/{db_id}/rps": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"retention policies"
|
||||||
|
],
|
||||||
|
"summary": "Retrieve all retention policies for a database",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "db_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the database",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Listing of all retention policies for a database",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicies"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Specified retention policy does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"retention policies"
|
||||||
|
],
|
||||||
|
"summary": "Create new retention policy for a database",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "db_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the database",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rp",
|
||||||
|
"in": "body",
|
||||||
|
"description": "Configuration options for the retention policy",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicy"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Retention Policy successfully created.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Data source id does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/sources/{id}/dbs/{db_id}/rps/{rp_id}": {
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"retention policies"
|
||||||
|
],
|
||||||
|
"summary": "Alter retention policy for a database",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "db_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the database",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rp_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the retention policy",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rp",
|
||||||
|
"in": "body",
|
||||||
|
"description": "Configuration options for the retention policy",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicy"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Retention Policy was altered",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Database or source does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"retention policies"
|
||||||
|
],
|
||||||
|
"summary": "Delete retention policy for a database",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the data source",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "db_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the database",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rp_id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the retention policy",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Retention Policy has been deleted",
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Data source id does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "A processing or an unexpected error.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/sources/{id}/kapacitors": {
|
"/sources/{id}/kapacitors": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -1912,6 +2235,70 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"Databases": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"databases"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"databases": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Database"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"example": {
|
||||||
|
"name": "NOAA_water_database",
|
||||||
|
"duration": "3d",
|
||||||
|
"replication": 3,
|
||||||
|
"shardDuration": "3h",
|
||||||
|
"links": {
|
||||||
|
"self": "/chronograf/v1/sources/1/dbs/NOAA_water_database",
|
||||||
|
"rps": "/chronograf/v1/sources/1/dbs/NOAA_water_database/rps"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The identifying name of the database",
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the duration of the default retention policy"
|
||||||
|
},
|
||||||
|
"replication": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "how many copies of the data are stored in the cluster"
|
||||||
|
},
|
||||||
|
"shardDuration": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the interval spanned by each shard group"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"self": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Self link mapping to this resource",
|
||||||
|
"format": "url"
|
||||||
|
},
|
||||||
|
"rps": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Link to retention policies for this database",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Kapacitors": {
|
"Kapacitors": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -2150,6 +2537,71 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RetentionPolicies": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"retentionPolicies"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"retentionPolicies": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RetentionPolicy": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"duration",
|
||||||
|
"replication"
|
||||||
|
],
|
||||||
|
"example": {
|
||||||
|
"name": "weekly",
|
||||||
|
"duration": "7d",
|
||||||
|
"replication": 1,
|
||||||
|
"shardDuration": "7d",
|
||||||
|
"default": true,
|
||||||
|
"links": {
|
||||||
|
"self": "/chronograf/v1/ousrces/1/dbs/NOAA_water_database/rps/liquid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The identifying name of the retention policy",
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the duration of the retention policy"
|
||||||
|
},
|
||||||
|
"replication": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "how many copies of the data are stored in the cluster"
|
||||||
|
},
|
||||||
|
"shardDuration": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the interval spanned by each shard group"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether this retention policy should be the default"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"self": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Self link mapping to this resource",
|
||||||
|
"format": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Rule": {
|
"Rule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"example": {
|
"example": {
|
||||||
|
|
|
@ -3,18 +3,33 @@ import reducer from 'src/admin/reducers/admin'
|
||||||
import {
|
import {
|
||||||
addUser,
|
addUser,
|
||||||
addRole,
|
addRole,
|
||||||
|
addDatabase,
|
||||||
|
addRetentionPolicy,
|
||||||
syncUser,
|
syncUser,
|
||||||
syncRole,
|
syncRole,
|
||||||
editUser,
|
editUser,
|
||||||
editRole,
|
editRole,
|
||||||
|
editDatabase,
|
||||||
|
editRetentionPolicy,
|
||||||
loadRoles,
|
loadRoles,
|
||||||
loadPermissions,
|
loadPermissions,
|
||||||
deleteRole,
|
deleteRole,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
removeDatabase,
|
||||||
|
removeRetentionPolicy,
|
||||||
filterRoles,
|
filterRoles,
|
||||||
filterUsers,
|
filterUsers,
|
||||||
|
addDatabaseDeleteCode,
|
||||||
|
removeDatabaseDeleteCode,
|
||||||
} from 'src/admin/actions'
|
} from 'src/admin/actions'
|
||||||
|
|
||||||
|
import {
|
||||||
|
NEW_DEFAULT_USER,
|
||||||
|
NEW_DEFAULT_ROLE,
|
||||||
|
NEW_DEFAULT_DATABASE,
|
||||||
|
NEW_EMPTY_RP,
|
||||||
|
} from 'src/admin/constants'
|
||||||
|
|
||||||
let state = undefined
|
let state = undefined
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
|
@ -58,14 +73,6 @@ const u2 = {
|
||||||
links: {self: '/chronograf/v1/sources/1/users/zerocool'},
|
links: {self: '/chronograf/v1/sources/1/users/zerocool'},
|
||||||
}
|
}
|
||||||
const users = [u1, u2]
|
const users = [u1, u2]
|
||||||
const newDefaultUser = {
|
|
||||||
name: '',
|
|
||||||
password: '',
|
|
||||||
roles: [],
|
|
||||||
permissions: [],
|
|
||||||
links: {self: ''},
|
|
||||||
isNew: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roles
|
// Roles
|
||||||
const r1 = {
|
const r1 = {
|
||||||
|
@ -103,20 +110,118 @@ const r2 = {
|
||||||
links: {self: '/chronograf/v1/sources/1/roles/l33tus3r'}
|
links: {self: '/chronograf/v1/sources/1/roles/l33tus3r'}
|
||||||
}
|
}
|
||||||
const roles = [r1, r2]
|
const roles = [r1, r2]
|
||||||
const newDefaultRole = {
|
|
||||||
name: '',
|
|
||||||
users: [],
|
|
||||||
permissions: [],
|
|
||||||
links: {self: ''},
|
|
||||||
isNew: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
const global = {scope: 'all', allowed: ['p1', 'p2']}
|
const global = {scope: 'all', allowed: ['p1', 'p2']}
|
||||||
const scoped = {scope: 'db1', allowed: ['p1', 'p3']}
|
const scoped = {scope: 'db1', allowed: ['p1', 'p3']}
|
||||||
const permissions = [global, scoped]
|
const permissions = [global, scoped]
|
||||||
|
|
||||||
|
// Databases && Retention Policies
|
||||||
|
const rp1 = {
|
||||||
|
name: 'rp1',
|
||||||
|
duration: '0',
|
||||||
|
replication: 2,
|
||||||
|
isDefault: true,
|
||||||
|
links: {self: '/chronograf/v1/sources/1/db/db1/rp/rp1'},
|
||||||
|
}
|
||||||
|
|
||||||
|
const db1 = {
|
||||||
|
name: 'db1',
|
||||||
|
links: {self: '/chronograf/v1/sources/1/db/db1'},
|
||||||
|
retentionPolicies: [rp1],
|
||||||
|
}
|
||||||
|
|
||||||
|
const db2 = {
|
||||||
|
name: 'db2',
|
||||||
|
links: {self: '/chronograf/v1/sources/1/db/db2'},
|
||||||
|
retentionPolicies: [],
|
||||||
|
deleteCode: 'DELETE',
|
||||||
|
}
|
||||||
|
|
||||||
describe('Admin.Reducers', () => {
|
describe('Admin.Reducers', () => {
|
||||||
|
describe('Databases', () => {
|
||||||
|
const state = {databases: [db1, db2]}
|
||||||
|
|
||||||
|
it('can add a database', () => {
|
||||||
|
const actual = reducer(state, addDatabase())
|
||||||
|
const expected = [
|
||||||
|
{...NEW_DEFAULT_DATABASE, isEditing: true},
|
||||||
|
db1,
|
||||||
|
db2,
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can edit a database', () => {
|
||||||
|
const updates = {name: 'dbOne'}
|
||||||
|
const actual = reducer(state, editDatabase(db1, updates))
|
||||||
|
const expected = [{...db1, ...updates}, db2]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove a database', () => {
|
||||||
|
const actual = reducer(state, removeDatabase(db1))
|
||||||
|
const expected = [db2]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can add a database delete code', () => {
|
||||||
|
const actual = reducer(state, addDatabaseDeleteCode(db1))
|
||||||
|
const expected = [
|
||||||
|
{...db1, deleteCode: ''},
|
||||||
|
db2,
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove the delete code', () => {
|
||||||
|
const actual = reducer(state, removeDatabaseDeleteCode(db2))
|
||||||
|
delete db2.deleteCode
|
||||||
|
const expected = [
|
||||||
|
db1,
|
||||||
|
db2,
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Retention Policies', () => {
|
||||||
|
const state = {databases: [db1]}
|
||||||
|
|
||||||
|
it('can add a retention policy', () => {
|
||||||
|
const actual = reducer(state, addRetentionPolicy(db1))
|
||||||
|
const expected = [
|
||||||
|
{...db1, retentionPolicies: [NEW_EMPTY_RP, rp1]},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove a retention policy', () => {
|
||||||
|
const actual = reducer(state, removeRetentionPolicy(db1, rp1))
|
||||||
|
const expected = [
|
||||||
|
{...db1, retentionPolicies: []},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can edit a retention policy', () => {
|
||||||
|
const updates = {name: 'rpOne', duration: '100y', replication: '42'}
|
||||||
|
const actual = reducer(state, editRetentionPolicy(db1, rp1, updates))
|
||||||
|
const expected = [
|
||||||
|
{...db1, retentionPolicies: [{...rp1, ...updates}]},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.databases).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('it can add a user', () => {
|
it('it can add a user', () => {
|
||||||
state = {
|
state = {
|
||||||
users: [
|
users: [
|
||||||
|
@ -127,7 +232,7 @@ describe('Admin.Reducers', () => {
|
||||||
const actual = reducer(state, addUser())
|
const actual = reducer(state, addUser())
|
||||||
const expected = {
|
const expected = {
|
||||||
users: [
|
users: [
|
||||||
{...newDefaultUser, isEditing: true},
|
{...NEW_DEFAULT_USER, isEditing: true},
|
||||||
u1,
|
u1,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -171,7 +276,7 @@ describe('Admin.Reducers', () => {
|
||||||
const actual = reducer(state, addRole())
|
const actual = reducer(state, addRole())
|
||||||
const expected = {
|
const expected = {
|
||||||
roles: [
|
roles: [
|
||||||
{...newDefaultRole, isEditing: true},
|
{...NEW_DEFAULT_ROLE, isEditing: true},
|
||||||
r1,
|
r1,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('Formatting helpers', () => {
|
||||||
it("returns 'infinite' for a retention policy with a value of '0'", () => {
|
it("returns 'infinite' for a retention policy with a value of '0'", () => {
|
||||||
const actual = formatRPDuration('0')
|
const actual = formatRPDuration('0')
|
||||||
|
|
||||||
expect(actual).to.equal('infinite');
|
expect(actual).to.equal('∞');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly formats retention policy durations', () => {
|
it('correctly formats retention policy durations', () => {
|
||||||
|
|
|
@ -2,15 +2,24 @@ import {
|
||||||
getUsers as getUsersAJAX,
|
getUsers as getUsersAJAX,
|
||||||
getRoles as getRolesAJAX,
|
getRoles as getRolesAJAX,
|
||||||
getPermissions as getPermissionsAJAX,
|
getPermissions as getPermissionsAJAX,
|
||||||
|
getDbsAndRps as getDbsAndRpsAJAX,
|
||||||
createUser as createUserAJAX,
|
createUser as createUserAJAX,
|
||||||
createRole as createRoleAJAX,
|
createRole as createRoleAJAX,
|
||||||
|
createDatabase as createDatabaseAJAX,
|
||||||
|
createRetentionPolicy as createRetentionPolicyAJAX,
|
||||||
deleteUser as deleteUserAJAX,
|
deleteUser as deleteUserAJAX,
|
||||||
deleteRole as deleteRoleAJAX,
|
deleteRole as deleteRoleAJAX,
|
||||||
|
deleteDatabase as deleteDatabaseAJAX,
|
||||||
|
deleteRetentionPolicy as deleteRetentionPolicyAJAX,
|
||||||
updateRole as updateRoleAJAX,
|
updateRole as updateRoleAJAX,
|
||||||
updateUser as updateUserAJAX,
|
updateUser as updateUserAJAX,
|
||||||
|
updateRetentionPolicy as updateRetentionPolicyAJAX,
|
||||||
} from 'src/admin/apis'
|
} from 'src/admin/apis'
|
||||||
|
|
||||||
import {killQuery as killQueryProxy} from 'shared/apis/metaQuery'
|
import {
|
||||||
|
killQuery as killQueryProxy,
|
||||||
|
} from 'shared/apis/metaQuery'
|
||||||
|
|
||||||
import {publishNotification} from 'src/shared/actions/notifications';
|
import {publishNotification} from 'src/shared/actions/notifications';
|
||||||
import {ADMIN_NOTIFICATION_DELAY} from 'src/admin/constants'
|
import {ADMIN_NOTIFICATION_DELAY} from 'src/admin/constants'
|
||||||
|
|
||||||
|
@ -35,6 +44,13 @@ export const loadPermissions = ({permissions}) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadDatabases = (databases) => ({
|
||||||
|
type: 'LOAD_DATABASES',
|
||||||
|
payload: {
|
||||||
|
databases,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const addUser = () => ({
|
export const addUser = () => ({
|
||||||
type: 'ADD_USER',
|
type: 'ADD_USER',
|
||||||
})
|
})
|
||||||
|
@ -43,6 +59,17 @@ export const addRole = () => ({
|
||||||
type: 'ADD_ROLE',
|
type: 'ADD_ROLE',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const addDatabase = () => ({
|
||||||
|
type: 'ADD_DATABASE',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addRetentionPolicy = (database) => ({
|
||||||
|
type: 'ADD_RETENTION_POLICY',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const syncUser = (staleUser, syncedUser) => ({
|
export const syncUser = (staleUser, syncedUser) => ({
|
||||||
type: 'SYNC_USER',
|
type: 'SYNC_USER',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -59,6 +86,24 @@ export const syncRole = (staleRole, syncedRole) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const syncDatabase = (stale, synced) => ({
|
||||||
|
type: 'SYNC_DATABASE',
|
||||||
|
payload: {
|
||||||
|
stale,
|
||||||
|
synced,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const syncRetentionPolicy = (database, stale, synced) => ({
|
||||||
|
type: 'SYNC_RETENTION_POLICY',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
stale,
|
||||||
|
synced,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
export const editUser = (user, updates) => ({
|
export const editUser = (user, updates) => ({
|
||||||
type: 'EDIT_USER',
|
type: 'EDIT_USER',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -75,6 +120,14 @@ export const editRole = (role, updates) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const editDatabase = (database, updates) => ({
|
||||||
|
type: 'EDIT_DATABASE',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
updates,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const killQuery = (queryID) => ({
|
export const killQuery = (queryID) => ({
|
||||||
type: 'KILL_QUERY',
|
type: 'KILL_QUERY',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -96,6 +149,7 @@ export const loadQueries = (queries) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: change to 'removeUser'
|
||||||
export const deleteUser = (user) => ({
|
export const deleteUser = (user) => ({
|
||||||
type: 'DELETE_USER',
|
type: 'DELETE_USER',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -103,6 +157,7 @@ export const deleteUser = (user) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: change to 'removeRole'
|
||||||
export const deleteRole = (role) => ({
|
export const deleteRole = (role) => ({
|
||||||
type: 'DELETE_ROLE',
|
type: 'DELETE_ROLE',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -110,6 +165,21 @@ export const deleteRole = (role) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const removeDatabase = (database) => ({
|
||||||
|
type: 'REMOVE_DATABASE',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeRetentionPolicy = (database, retentionPolicy) => ({
|
||||||
|
type: 'REMOVE_RETENTION_POLICY',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const filterUsers = (text) => ({
|
export const filterUsers = (text) => ({
|
||||||
type: 'FILTER_USERS',
|
type: 'FILTER_USERS',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -124,6 +194,29 @@ export const filterRoles = (text) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const addDatabaseDeleteCode = (database) => ({
|
||||||
|
type: 'ADD_DATABASE_DELETE_CODE',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeDatabaseDeleteCode = (database) => ({
|
||||||
|
type: 'REMOVE_DATABASE_DELETE_CODE',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const editRetentionPolicy = (database, retentionPolicy, updates) => ({
|
||||||
|
type: 'EDIT_RETENTION_POLICY',
|
||||||
|
payload: {
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
updates,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// async actions
|
// async actions
|
||||||
export const loadUsersAsync = (url) => async (dispatch) => {
|
export const loadUsersAsync = (url) => async (dispatch) => {
|
||||||
const {data} = await getUsersAJAX(url)
|
const {data} = await getUsersAJAX(url)
|
||||||
|
@ -140,6 +233,11 @@ export const loadPermissionsAsync = (url) => async (dispatch) => {
|
||||||
dispatch(loadPermissions(data))
|
dispatch(loadPermissions(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadDBsAndRPsAsync = (url) => async (dispatch) => {
|
||||||
|
const {data: {databases}} = await getDbsAndRpsAJAX(url)
|
||||||
|
dispatch(loadDatabases(databases))
|
||||||
|
}
|
||||||
|
|
||||||
export const createUserAsync = (url, user) => async (dispatch) => {
|
export const createUserAsync = (url, user) => async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
const {data} = await createUserAJAX(url, user)
|
const {data} = await createUserAJAX(url, user)
|
||||||
|
@ -164,6 +262,41 @@ export const createRoleAsync = (url, role) => async (dispatch) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createDatabaseAsync = (url, database) => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const {data} = await createDatabaseAJAX(url, database)
|
||||||
|
dispatch(syncDatabase(database, data))
|
||||||
|
dispatch(publishNotification('success', 'Database created successfully'))
|
||||||
|
} catch (error) {
|
||||||
|
// undo optimistic update
|
||||||
|
dispatch(publishNotification('error', `Failed to create database: ${error.data.message}`))
|
||||||
|
setTimeout(() => dispatch(removeDatabase(database)), ADMIN_NOTIFICATION_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRetentionPolicyAsync = (database, retentionPolicy) => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const {data} = await createRetentionPolicyAJAX(database.links.retentionPolicies, retentionPolicy)
|
||||||
|
dispatch(publishNotification('success', 'Retention policy created successfully'))
|
||||||
|
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
|
||||||
|
} catch (error) {
|
||||||
|
// undo optimistic update
|
||||||
|
dispatch(publishNotification('error', `Failed to create retention policy: ${error.data.message}`))
|
||||||
|
setTimeout(() => dispatch(removeRetentionPolicy(database, retentionPolicy)), ADMIN_NOTIFICATION_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRetentionPolicyAsync = (database, retentionPolicy, updates) => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(editRetentionPolicy(database, retentionPolicy, updates))
|
||||||
|
const {data} = await updateRetentionPolicyAJAX(retentionPolicy.links.self, updates)
|
||||||
|
dispatch(publishNotification('success', 'Retention policy updated successfully'))
|
||||||
|
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(publishNotification('error', `Failed to update retention policy: ${error.data.message}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const killQueryAsync = (source, queryID) => (dispatch) => {
|
export const killQueryAsync = (source, queryID) => (dispatch) => {
|
||||||
// optimistic update
|
// optimistic update
|
||||||
dispatch(killQuery(queryID))
|
dispatch(killQuery(queryID))
|
||||||
|
@ -189,6 +322,26 @@ export const deleteUserAsync = (user, addFlashMessage) => (dispatch) => {
|
||||||
deleteUserAJAX(user.links.self, addFlashMessage, user.name)
|
deleteUserAJAX(user.links.self, addFlashMessage, user.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteDatabaseAsync = (database) => async (dispatch) => {
|
||||||
|
dispatch(removeDatabase(database))
|
||||||
|
dispatch(publishNotification('success', 'Database deleted'))
|
||||||
|
try {
|
||||||
|
await deleteDatabaseAJAX(database.links.self)
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(publishNotification('error', `Failed to delete database: ${error.data.message}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteRetentionPolicyAsync = (database, retentionPolicy) => async (dispatch) => {
|
||||||
|
dispatch(removeRetentionPolicy(database, retentionPolicy))
|
||||||
|
dispatch(publishNotification('success', `Retention policy ${retentionPolicy.name} deleted`))
|
||||||
|
try {
|
||||||
|
await deleteRetentionPolicyAJAX(retentionPolicy.links.self)
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(publishNotification('error', `Failed to delete retentionPolicy: ${error.data.message}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const updateRoleUsersAsync = (role, users) => async (dispatch) => {
|
export const updateRoleUsersAsync = (role, users) => async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
const {data} = await updateRoleAJAX(role.links.self, users, role.permissions)
|
const {data} = await updateRoleAJAX(role.links.self, users, role.permissions)
|
||||||
|
|
|
@ -36,6 +36,18 @@ export const getPermissions = async (url) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDbsAndRps = async (url) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'GET',
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const createUser = async (url, user) => {
|
export const createUser = async (url, user) => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
|
@ -60,6 +72,41 @@ export const createRole = async (url, role) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createDatabase = async (url, database) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
data: database,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRetentionPolicy = async (url, retentionPolicy) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
data: retentionPolicy,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteRetentionPolicy = async (url) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'DELETE',
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteRole = async (url, addFlashMessage, rolename) => {
|
export const deleteRole = async (url, addFlashMessage, rolename) => {
|
||||||
try {
|
try {
|
||||||
const response = await AJAX({
|
const response = await AJAX({
|
||||||
|
@ -100,6 +147,18 @@ export const deleteUser = async (url, addFlashMessage, username) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteDatabase = async (url) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'DELETE',
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const updateRole = async (url, users, permissions) => {
|
export const updateRole = async (url, users, permissions) => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
|
@ -131,3 +190,18 @@ export const updateUser = async (url, roles, permissions) => {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateRetentionPolicy = async (url, retentionPolicy) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'PUT',
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
...retentionPolicy,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'src/shared/components/Tabs';
|
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'src/shared/components/Tabs'
|
||||||
import UsersTable from 'src/admin/components/UsersTable'
|
import UsersTable from 'src/admin/components/UsersTable'
|
||||||
import RolesTable from 'src/admin/components/RolesTable'
|
import RolesTable from 'src/admin/components/RolesTable'
|
||||||
import QueriesPage from 'src/admin/containers/QueriesPage'
|
import QueriesPage from 'src/admin/containers/QueriesPage'
|
||||||
|
import DatabaseManagerPage from 'src/admin/containers/DatabaseManagerPage'
|
||||||
|
|
||||||
const AdminTabs = ({
|
const AdminTabs = ({
|
||||||
users,
|
users,
|
||||||
|
@ -29,6 +30,10 @@ const AdminTabs = ({
|
||||||
onUpdateUserPermissions,
|
onUpdateUserPermissions,
|
||||||
}) => {
|
}) => {
|
||||||
let tabs = [
|
let tabs = [
|
||||||
|
{
|
||||||
|
type: 'DB Management',
|
||||||
|
component: (<DatabaseManagerPage source={source} />),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'Users',
|
type: 'Users',
|
||||||
component: (
|
component: (
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import DatabaseTable from 'src/admin/components/DatabaseTable'
|
||||||
|
|
||||||
|
const DatabaseManager = ({
|
||||||
|
databases,
|
||||||
|
notify,
|
||||||
|
isRFDisplayed,
|
||||||
|
isAddDBDisabled,
|
||||||
|
addDatabase,
|
||||||
|
onEditDatabase,
|
||||||
|
onKeyDownDatabase,
|
||||||
|
onCancelDatabase,
|
||||||
|
onConfirmDatabase,
|
||||||
|
onStartDeleteDatabase,
|
||||||
|
onDatabaseDeleteConfirm,
|
||||||
|
onAddRetentionPolicy,
|
||||||
|
onStopEditRetentionPolicy,
|
||||||
|
onCancelRetentionPolicy,
|
||||||
|
onCreateRetentionPolicy,
|
||||||
|
onUpdateRetentionPolicy,
|
||||||
|
onRemoveRetentionPolicy,
|
||||||
|
onDeleteRetentionPolicy,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="panel panel-info">
|
||||||
|
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||||
|
<h2 className="panel-title">{databases.length === 1 ? '1 Database' : `${databases.length} Databases`}</h2>
|
||||||
|
<div className="btn btn-sm btn-primary" disabled={isAddDBDisabled} onClick={addDatabase}>Create Database</div>
|
||||||
|
</div>
|
||||||
|
<div className="panel-body">
|
||||||
|
{
|
||||||
|
databases.map(db =>
|
||||||
|
<DatabaseTable
|
||||||
|
key={db.links.self}
|
||||||
|
database={db}
|
||||||
|
notify={notify}
|
||||||
|
isRFDisplayed={isRFDisplayed}
|
||||||
|
onEditDatabase={onEditDatabase}
|
||||||
|
onKeyDownDatabase={onKeyDownDatabase}
|
||||||
|
onCancelDatabase={onCancelDatabase}
|
||||||
|
onConfirmDatabase={onConfirmDatabase}
|
||||||
|
onStartDeleteDatabase={onStartDeleteDatabase}
|
||||||
|
onDatabaseDeleteConfirm={onDatabaseDeleteConfirm}
|
||||||
|
onAddRetentionPolicy={onAddRetentionPolicy}
|
||||||
|
onStopEditRetentionPolicy={onStopEditRetentionPolicy}
|
||||||
|
onCancelRetentionPolicy={onCancelRetentionPolicy}
|
||||||
|
onCreateRetentionPolicy={onCreateRetentionPolicy}
|
||||||
|
onUpdateRetentionPolicy={onUpdateRetentionPolicy}
|
||||||
|
onRemoveRetentionPolicy={onRemoveRetentionPolicy}
|
||||||
|
onDeleteRetentionPolicy={onDeleteRetentionPolicy}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
arrayOf,
|
||||||
|
bool,
|
||||||
|
func,
|
||||||
|
shape,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
DatabaseManager.propTypes = {
|
||||||
|
databases: arrayOf(shape()),
|
||||||
|
notify: func,
|
||||||
|
addDatabase: func,
|
||||||
|
isRFDisplayed: bool,
|
||||||
|
isAddDBDisabled: bool,
|
||||||
|
onEditDatabase: func,
|
||||||
|
onKeyDownDatabase: func,
|
||||||
|
onCancelDatabase: func,
|
||||||
|
onConfirmDatabase: func,
|
||||||
|
onStartDeleteDatabase: func,
|
||||||
|
onDatabaseDeleteConfirm: func,
|
||||||
|
onAddRetentionPolicy: func,
|
||||||
|
onEditRetentionPolicy: func,
|
||||||
|
onStopEditRetentionPolicy: func,
|
||||||
|
onCancelRetentionPolicy: func,
|
||||||
|
onCreateRetentionPolicy: func,
|
||||||
|
onUpdateRetentionPolicy: func,
|
||||||
|
onRemoveRetentionPolicy: func,
|
||||||
|
onDeleteRetentionPolicy: func,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseManager
|
||||||
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
import {formatRPDuration} from 'utils/formatting'
|
||||||
|
import YesNoButtons from 'src/shared/components/YesNoButtons'
|
||||||
|
import onClickOutside from 'react-onclickoutside'
|
||||||
|
|
||||||
|
class DatabaseRow extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
isEditing: false,
|
||||||
|
isDeleting: false,
|
||||||
|
}
|
||||||
|
this.handleKeyDown = ::this.handleKeyDown
|
||||||
|
this.handleClickOutside = ::this.handleClickOutside
|
||||||
|
this.handleStartEdit = ::this.handleStartEdit
|
||||||
|
this.handleEndEdit = ::this.handleEndEdit
|
||||||
|
this.handleCreate = ::this.handleCreate
|
||||||
|
this.handleUpdate = ::this.handleUpdate
|
||||||
|
this.getInputValues = ::this.getInputValues
|
||||||
|
this.handleStartDelete = ::this.handleStartDelete
|
||||||
|
this.handleEndDelete = ::this.handleEndDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
if (this.props.retentionPolicy.isNew) {
|
||||||
|
this.setState({isEditing: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onRemove,
|
||||||
|
retentionPolicy: {name, duration, replication, isDefault, isNew},
|
||||||
|
retentionPolicy,
|
||||||
|
database,
|
||||||
|
onDelete,
|
||||||
|
isRFDisplayed,
|
||||||
|
} = this.props
|
||||||
|
const {isEditing, isDeleting} = this.state
|
||||||
|
|
||||||
|
const formattedDuration = formatRPDuration(duration)
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{
|
||||||
|
isNew ?
|
||||||
|
<div className="admin-table--edit-cell">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
type="text"
|
||||||
|
defaultValue={name}
|
||||||
|
placeholder="give it a name"
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e, database)}
|
||||||
|
ref={(r) => this.name = r}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
</div> :
|
||||||
|
<div className="admin-table--edit-cell">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="admin-table--edit-cell">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
defaultValue={formattedDuration}
|
||||||
|
placeholder="how long should data last"
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e, database)}
|
||||||
|
ref={(r) => this.duration = r}
|
||||||
|
autoFocus={!isNew}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style={isRFDisplayed ? {} : {display: 'none'}}>
|
||||||
|
<div className="admin-table--edit-cell">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
name="name"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
defaultValue={replication || 1}
|
||||||
|
placeholder="how many nodes do you have"
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e, database)}
|
||||||
|
ref={(r) => this.replication = r}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="text-right">
|
||||||
|
<YesNoButtons
|
||||||
|
onConfirm={isNew ? this.handleCreate : this.handleUpdate}
|
||||||
|
onCancel={isNew ? () => onRemove(database, retentionPolicy) : this.handleEndEdit}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{name} {isDefault ? <span className="default-source-label">default</span> : null}</td>
|
||||||
|
<td onClick={this.handleStartEdit}>{formattedDuration}</td>
|
||||||
|
{isRFDisplayed ? <td onClick={this.handleStartEdit}>{replication}</td> : null}
|
||||||
|
<td className="text-right">
|
||||||
|
{
|
||||||
|
isDeleting ?
|
||||||
|
<YesNoButtons onConfirm={() => onDelete(database, retentionPolicy)} onCancel={this.handleEndDelete} /> :
|
||||||
|
this.renderDeleteButton()
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDeleteButton() {
|
||||||
|
if (!this.props.isDeletable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className="btn btn-xs btn-danger admin-table--delete" onClick={this.handleStartDelete}>
|
||||||
|
{`Delete ${name}`}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOutside() {
|
||||||
|
const {database, retentionPolicy, onRemove} = this.props
|
||||||
|
if (retentionPolicy.isNew) {
|
||||||
|
onRemove(database, retentionPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleEndEdit()
|
||||||
|
this.handleEndDelete()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStartEdit() {
|
||||||
|
this.setState({isEditing: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEndEdit() {
|
||||||
|
this.setState({isEditing: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStartDelete() {
|
||||||
|
this.setState({isDeleting: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEndDelete() {
|
||||||
|
this.setState({isDeleting: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCreate() {
|
||||||
|
const {database, retentionPolicy, onCreate} = this.props
|
||||||
|
const validInputs = this.getInputValues()
|
||||||
|
if (!validInputs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreate(database, {...retentionPolicy, ...validInputs})
|
||||||
|
this.handleEndEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdate() {
|
||||||
|
const {database, retentionPolicy, onUpdate} = this.props
|
||||||
|
const validInputs = this.getInputValues()
|
||||||
|
if (!validInputs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(database, retentionPolicy, validInputs)
|
||||||
|
this.handleEndEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(e) {
|
||||||
|
const {key} = e
|
||||||
|
const {retentionPolicy, database, onRemove} = this.props
|
||||||
|
|
||||||
|
|
||||||
|
if (key === 'Escape') {
|
||||||
|
if (retentionPolicy.isNew) {
|
||||||
|
onRemove(database, retentionPolicy)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleEndEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'Enter') {
|
||||||
|
if (retentionPolicy.isNew) {
|
||||||
|
this.handleCreate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputValues() {
|
||||||
|
let duration = this.duration.value.trim()
|
||||||
|
const replication = +this.replication.value.trim()
|
||||||
|
const {notify, retentionPolicy: {name}} = this.props
|
||||||
|
|
||||||
|
if (!duration || !replication) {
|
||||||
|
notify('error', 'Fields cannot be empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration === '∞') {
|
||||||
|
duration = 'INF'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
duration,
|
||||||
|
replication,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
bool,
|
||||||
|
func,
|
||||||
|
number,
|
||||||
|
shape,
|
||||||
|
string,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
DatabaseRow.propTypes = {
|
||||||
|
retentionPolicy: shape({
|
||||||
|
name: string,
|
||||||
|
duration: string,
|
||||||
|
replication: number,
|
||||||
|
isDefault: bool,
|
||||||
|
isEditing: bool,
|
||||||
|
}),
|
||||||
|
isDeletable: bool,
|
||||||
|
database: shape(),
|
||||||
|
onRemove: func,
|
||||||
|
onCreate: func,
|
||||||
|
onUpdate: func,
|
||||||
|
onDelete: func,
|
||||||
|
notify: func,
|
||||||
|
isRFDisplayed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default onClickOutside(DatabaseRow)
|
|
@ -0,0 +1,234 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import DatabaseRow from 'src/admin/components/DatabaseRow'
|
||||||
|
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
|
||||||
|
|
||||||
|
const {
|
||||||
|
func,
|
||||||
|
shape,
|
||||||
|
bool,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
const DatabaseTable = ({
|
||||||
|
database,
|
||||||
|
notify,
|
||||||
|
isRFDisplayed,
|
||||||
|
onEditDatabase,
|
||||||
|
onKeyDownDatabase,
|
||||||
|
onCancelDatabase,
|
||||||
|
onConfirmDatabase,
|
||||||
|
onStartDeleteDatabase,
|
||||||
|
onDatabaseDeleteConfirm,
|
||||||
|
onAddRetentionPolicy,
|
||||||
|
onCreateRetentionPolicy,
|
||||||
|
onUpdateRetentionPolicy,
|
||||||
|
onRemoveRetentionPolicy,
|
||||||
|
onDeleteRetentionPolicy,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="db-manager">
|
||||||
|
<DatabaseTableHeader
|
||||||
|
database={database}
|
||||||
|
isAddRPDisabled={!!database.retentionPolicies.some(rp => rp.isNew)}
|
||||||
|
onEdit={onEditDatabase}
|
||||||
|
onKeyDown={onKeyDownDatabase}
|
||||||
|
onCancel={onCancelDatabase}
|
||||||
|
onConfirm={onConfirmDatabase}
|
||||||
|
onStartDelete={onStartDeleteDatabase}
|
||||||
|
onDatabaseDeleteConfirm={onDatabaseDeleteConfirm}
|
||||||
|
onAddRetentionPolicy={onAddRetentionPolicy}
|
||||||
|
onDeleteRetentionPolicy={onDeleteRetentionPolicy}
|
||||||
|
/>
|
||||||
|
<div className="db-manager-table">
|
||||||
|
<table className="table v-center admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Retention Policy</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
{isRFDisplayed ? <th>Replication Factor</th> : null}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
database.retentionPolicies.map(rp => {
|
||||||
|
return (
|
||||||
|
<DatabaseRow
|
||||||
|
key={rp.links.self}
|
||||||
|
notify={notify}
|
||||||
|
database={database}
|
||||||
|
retentionPolicy={rp}
|
||||||
|
onCreate={onCreateRetentionPolicy}
|
||||||
|
onUpdate={onUpdateRetentionPolicy}
|
||||||
|
onRemove={onRemoveRetentionPolicy}
|
||||||
|
onDelete={onDeleteRetentionPolicy}
|
||||||
|
isRFDisplayed={isRFDisplayed}
|
||||||
|
isDeletable={database.retentionPolicies.length > 1}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseTable.propTypes = {
|
||||||
|
onEditDatabase: func,
|
||||||
|
database: shape(),
|
||||||
|
notify: func,
|
||||||
|
isRFDisplayed: bool,
|
||||||
|
isAddRPDisabled: bool,
|
||||||
|
onKeyDownDatabase: func,
|
||||||
|
onCancelDatabase: func,
|
||||||
|
onConfirmDatabase: func,
|
||||||
|
onStartDeleteDatabase: func,
|
||||||
|
onDatabaseDeleteConfirm: func,
|
||||||
|
onAddRetentionPolicy: func,
|
||||||
|
onCancelRetentionPolicy: func,
|
||||||
|
onCreateRetentionPolicy: func,
|
||||||
|
onUpdateRetentionPolicy: func,
|
||||||
|
onRemoveRetentionPolicy: func,
|
||||||
|
onDeleteRetentionPolicy: func,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatabaseTableHeader = ({
|
||||||
|
database,
|
||||||
|
onEdit,
|
||||||
|
onKeyDown,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
onStartDelete,
|
||||||
|
onDatabaseDeleteConfirm,
|
||||||
|
onAddRetentionPolicy,
|
||||||
|
isAddRPDisabled,
|
||||||
|
}) => {
|
||||||
|
if (database.isEditing) {
|
||||||
|
return (
|
||||||
|
<EditHeader
|
||||||
|
database={database}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
onCancel={onCancel}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header
|
||||||
|
database={database}
|
||||||
|
onStartDelete={onStartDelete}
|
||||||
|
onDatabaseDeleteConfirm={onDatabaseDeleteConfirm}
|
||||||
|
onAddRetentionPolicy={onAddRetentionPolicy}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
onCancel={onCancel}
|
||||||
|
isAddRPDisabled={isAddRPDisabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseTableHeader.propTypes = {
|
||||||
|
onEdit: func,
|
||||||
|
database: shape(),
|
||||||
|
onKeyDown: func,
|
||||||
|
onCancel: func,
|
||||||
|
onConfirm: func,
|
||||||
|
onStartDelete: func,
|
||||||
|
onDatabaseDeleteConfirm: func,
|
||||||
|
onAddRetentionPolicy: func,
|
||||||
|
isAddRPDisabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
database,
|
||||||
|
onStartDelete,
|
||||||
|
onDatabaseDeleteConfirm,
|
||||||
|
onAddRetentionPolicy,
|
||||||
|
isAddRPDisabled,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const confirmStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = (
|
||||||
|
<div className="text-right">
|
||||||
|
{
|
||||||
|
database.name === '_internal' ? null :
|
||||||
|
<button className="btn btn-xs btn-danger" onClick={() => onStartDelete(database)}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button className="btn btn-xs btn-primary" disabled={isAddRPDisabled} onClick={() => onAddRetentionPolicy(database)}>
|
||||||
|
Add retention policy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteConfirm = (
|
||||||
|
<div style={confirmStyle}>
|
||||||
|
<div className="admin-table--delete-cell">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={database.deleteCode || ''}
|
||||||
|
placeholder={`DELETE ${database.name}`}
|
||||||
|
onChange={(e) => onDatabaseDeleteConfirm(database, e)}
|
||||||
|
onKeyDown={(e) => onDatabaseDeleteConfirm(database, e)}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ConfirmButtons item={database} onConfirm={onConfirm} onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="db-manager-header">
|
||||||
|
<h4>{database.name}</h4>
|
||||||
|
{database.hasOwnProperty('deleteCode') ? deleteConfirm : buttons}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
database: shape(),
|
||||||
|
onStartDelete: func,
|
||||||
|
onDatabaseDeleteConfirm: func,
|
||||||
|
onAddRetentionPolicy: func,
|
||||||
|
isAddRPDisabled: bool,
|
||||||
|
onConfirm: func,
|
||||||
|
onCancel: func,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (
|
||||||
|
<div className="db-manager-header-edit">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={database.name}
|
||||||
|
placeholder="Database name"
|
||||||
|
onChange={(e) => onEdit(database, {name: e.target.value})}
|
||||||
|
onKeyDown={(e) => onKeyDown(e, database)}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<ConfirmButtons item={database} onConfirm={onConfirm} onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
EditHeader.propTypes = {
|
||||||
|
database: shape(),
|
||||||
|
onEdit: func,
|
||||||
|
onKeyDown: func,
|
||||||
|
onCancel: func,
|
||||||
|
onConfirm: func,
|
||||||
|
isRFDisplayed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseTable
|
|
@ -9,3 +9,44 @@ export const TIMES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ADMIN_NOTIFICATION_DELAY = 1500 // milliseconds
|
export const ADMIN_NOTIFICATION_DELAY = 1500 // milliseconds
|
||||||
|
|
||||||
|
export const NEW_DEFAULT_USER = {
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
roles: [],
|
||||||
|
permissions: [],
|
||||||
|
links: {self: ''},
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEW_DEFAULT_ROLE = {
|
||||||
|
name: '',
|
||||||
|
permissions: [],
|
||||||
|
users: [],
|
||||||
|
links: {self: ''},
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEW_DEFAULT_RP = {
|
||||||
|
name: 'autogen',
|
||||||
|
duration: '0',
|
||||||
|
replication: 2,
|
||||||
|
isDefault: true,
|
||||||
|
links: {self: ''},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEW_EMPTY_RP = {
|
||||||
|
name: '',
|
||||||
|
duration: '',
|
||||||
|
replication: 0,
|
||||||
|
links: {self: ''},
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEW_DEFAULT_DATABASE = {
|
||||||
|
name: '',
|
||||||
|
isNew: true,
|
||||||
|
retentionPolicies: [NEW_DEFAULT_RP],
|
||||||
|
links: {self: ''},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
import {connect} from 'react-redux'
|
||||||
|
import {bindActionCreators} from 'redux'
|
||||||
|
|
||||||
|
import * as adminActionCreators from 'src/admin/actions'
|
||||||
|
import DatabaseManager from 'src/admin/components/DatabaseManager'
|
||||||
|
import {publishNotification} from 'src/shared/actions/notifications';
|
||||||
|
|
||||||
|
class DatabaseManagerPage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.handleKeyDownDatabase = ::this.handleKeyDownDatabase
|
||||||
|
this.handleDatabaseDeleteConfirm = ::this.handleDatabaseDeleteConfirm
|
||||||
|
this.handleCreateDatabase = ::this.handleCreateDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {source: {links: {databases}}, actions} = this.props
|
||||||
|
|
||||||
|
actions.loadDBsAndRPsAsync(databases)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {source, databases, actions, notify} = this.props
|
||||||
|
return (
|
||||||
|
|
||||||
|
<DatabaseManager
|
||||||
|
databases={databases}
|
||||||
|
notify={notify}
|
||||||
|
isRFDisplayed={!!source.metaUrl}
|
||||||
|
isAddDBDisabled={!!databases.some(db => db.isEditing)}
|
||||||
|
onKeyDownDatabase={this.handleKeyDownDatabase}
|
||||||
|
onDatabaseDeleteConfirm={this.handleDatabaseDeleteConfirm}
|
||||||
|
addDatabase={actions.addDatabase}
|
||||||
|
onEditDatabase={actions.editDatabase}
|
||||||
|
onCancelDatabase={actions.removeDatabase}
|
||||||
|
onConfirmDatabase={this.handleCreateDatabase}
|
||||||
|
onStartDeleteDatabase={actions.addDatabaseDeleteCode}
|
||||||
|
onAddRetentionPolicy={actions.addRetentionPolicy}
|
||||||
|
onCreateRetentionPolicy={actions.createRetentionPolicyAsync}
|
||||||
|
onUpdateRetentionPolicy={actions.updateRetentionPolicyAsync}
|
||||||
|
onRemoveRetentionPolicy={actions.removeRetentionPolicy}
|
||||||
|
onDeleteRetentionPolicy={actions.deleteRetentionPolicyAsync}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCreateDatabase(database) {
|
||||||
|
const {actions, notify, source} = this.props
|
||||||
|
|
||||||
|
if (!database.name) {
|
||||||
|
return notify('error', 'Database name cannot be blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.createDatabaseAsync(source.links.databases, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDownDatabase(e, database) {
|
||||||
|
const {key} = e
|
||||||
|
const {actions, notify, source} = this.props
|
||||||
|
|
||||||
|
if (key === 'Escape') {
|
||||||
|
actions.removeDatabase(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'Enter') {
|
||||||
|
if (!database.name) {
|
||||||
|
return notify('error', 'Database name cannot be blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.createDatabaseAsync(source.links.databases, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDatabaseDeleteConfirm(database, e) {
|
||||||
|
const {key, target: {value}} = e
|
||||||
|
const {actions, notify} = this.props
|
||||||
|
|
||||||
|
if (key === 'Escape') {
|
||||||
|
return actions.removeDatabaseDeleteCode(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'Enter') {
|
||||||
|
if (database.deleteCode !== `DELETE ${database.name}`) {
|
||||||
|
return notify('error', `Please type DELETE ${database.name} to confirm`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions.deleteDatabaseAsync(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.editDatabase(database, {deleteCode: value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
arrayOf,
|
||||||
|
bool,
|
||||||
|
func,
|
||||||
|
number,
|
||||||
|
shape,
|
||||||
|
string,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
DatabaseManagerPage.propTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
databases: arrayOf(shape({
|
||||||
|
name: string,
|
||||||
|
isEditing: bool,
|
||||||
|
})),
|
||||||
|
retentionPolicies: arrayOf(arrayOf(shape({
|
||||||
|
name: string,
|
||||||
|
duration: string,
|
||||||
|
replication: number,
|
||||||
|
isDefault: bool,
|
||||||
|
}))),
|
||||||
|
actions: shape({
|
||||||
|
addRetentionPolicy: func,
|
||||||
|
loadDBsAndRPsAsync: func,
|
||||||
|
createDatabaseAsync: func,
|
||||||
|
createRetentionPolicyAsync: func,
|
||||||
|
addDatabase: func,
|
||||||
|
removeDatabase: func,
|
||||||
|
startDeleteDatabase: func,
|
||||||
|
removeDatabaseDeleteCode: func,
|
||||||
|
removeRetentionPolicy: func,
|
||||||
|
deleteRetentionPolicyAsync: func,
|
||||||
|
}),
|
||||||
|
notify: func,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = ({admin: {databases, retentionPolicies}}) => ({
|
||||||
|
databases,
|
||||||
|
retentionPolicies,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
actions: bindActionCreators(adminActionCreators, dispatch),
|
||||||
|
notify: bindActionCreators(publishNotification, dispatch),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DatabaseManagerPage)
|
|
@ -1,20 +1,10 @@
|
||||||
import reject from 'lodash/reject'
|
import reject from 'lodash/reject'
|
||||||
|
import {
|
||||||
const newDefaultUser = {
|
NEW_DEFAULT_USER,
|
||||||
name: '',
|
NEW_DEFAULT_ROLE,
|
||||||
password: '',
|
NEW_DEFAULT_DATABASE,
|
||||||
roles: [],
|
NEW_EMPTY_RP,
|
||||||
permissions: [],
|
} from 'src/admin/constants'
|
||||||
links: {self: ''},
|
|
||||||
isNew: true,
|
|
||||||
}
|
|
||||||
const newDefaultRole = {
|
|
||||||
name: '',
|
|
||||||
permissions: [],
|
|
||||||
users: [],
|
|
||||||
links: {self: ''},
|
|
||||||
isNew: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
users: null,
|
users: null,
|
||||||
|
@ -22,6 +12,7 @@ const initialState = {
|
||||||
permissions: [],
|
permissions: [],
|
||||||
queries: [],
|
queries: [],
|
||||||
queryIDToKill: null,
|
queryIDToKill: null,
|
||||||
|
databases: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function admin(state = initialState, action) {
|
export default function admin(state = initialState, action) {
|
||||||
|
@ -38,8 +29,12 @@ export default function admin(state = initialState, action) {
|
||||||
return {...state, ...action.payload}
|
return {...state, ...action.payload}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'LOAD_DATABASES': {
|
||||||
|
return {...state, ...action.payload}
|
||||||
|
}
|
||||||
|
|
||||||
case 'ADD_USER': {
|
case 'ADD_USER': {
|
||||||
const newUser = {...newDefaultUser, isEditing: true}
|
const newUser = {...NEW_DEFAULT_USER, isEditing: true}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
users: [
|
users: [
|
||||||
|
@ -50,7 +45,7 @@ export default function admin(state = initialState, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'ADD_ROLE': {
|
case 'ADD_ROLE': {
|
||||||
const newRole = {...newDefaultRole, isEditing: true}
|
const newRole = {...NEW_DEFAULT_ROLE, isEditing: true}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
roles: [
|
roles: [
|
||||||
|
@ -60,6 +55,29 @@ export default function admin(state = initialState, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'ADD_DATABASE': {
|
||||||
|
const newDatabase = {...NEW_DEFAULT_DATABASE, isEditing: true}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
databases: [
|
||||||
|
newDatabase,
|
||||||
|
...state.databases,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_RETENTION_POLICY': {
|
||||||
|
const {database} = action.payload
|
||||||
|
const databases = state.databases.map(db =>
|
||||||
|
db.links.self === database.links.self ?
|
||||||
|
{...database, retentionPolicies: [{...NEW_EMPTY_RP}, ...database.retentionPolicies]}
|
||||||
|
: db
|
||||||
|
)
|
||||||
|
|
||||||
|
return {...state, databases}
|
||||||
|
}
|
||||||
|
|
||||||
case 'SYNC_USER': {
|
case 'SYNC_USER': {
|
||||||
const {staleUser, syncedUser} = action.payload
|
const {staleUser, syncedUser} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
|
@ -76,6 +94,27 @@ export default function admin(state = initialState, action) {
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SYNC_DATABASE': {
|
||||||
|
const {stale, synced} = action.payload
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === stale.links.self ? {...synced} : db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SYNC_RETENTION_POLICY': {
|
||||||
|
const {database, stale, synced} = action.payload
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === database.links.self ? {
|
||||||
|
...db,
|
||||||
|
retentionPolicies: db.retentionPolicies.map(rp => rp.links.self === stale.links.self ? {...synced} : rp),
|
||||||
|
} : db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
case 'EDIT_USER': {
|
case 'EDIT_USER': {
|
||||||
const {user, updates} = action.payload
|
const {user, updates} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
|
@ -92,6 +131,28 @@ export default function admin(state = initialState, action) {
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'EDIT_DATABASE': {
|
||||||
|
const {database, updates} = action.payload
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === database.links.self ? {...db, ...updates} : db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'EDIT_RETENTION_POLICY': {
|
||||||
|
const {database, retentionPolicy, updates} = action.payload
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === database.links.self ? {
|
||||||
|
...db,
|
||||||
|
retentionPolicies: db.retentionPolicies.map(rp => rp.links.self === retentionPolicy.links.self ? {...rp, ...updates} : rp),
|
||||||
|
} : db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
case 'DELETE_USER': {
|
case 'DELETE_USER': {
|
||||||
const {user} = action.payload
|
const {user} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
|
@ -110,6 +171,48 @@ export default function admin(state = initialState, action) {
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'REMOVE_DATABASE': {
|
||||||
|
const {database} = action.payload
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.filter(db => db.links.self !== database.links.self),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'REMOVE_RETENTION_POLICY': {
|
||||||
|
const {database, retentionPolicy} = action.payload
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === database.links.self ? {
|
||||||
|
...db,
|
||||||
|
retentionPolicies: db.retentionPolicies.filter(rp => rp.links.self !== retentionPolicy.links.self),
|
||||||
|
}
|
||||||
|
: db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_DATABASE_DELETE_CODE': {
|
||||||
|
const {database} = action.payload
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === database.links.self ? {...db, deleteCode: ''} : db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'REMOVE_DATABASE_DELETE_CODE': {
|
||||||
|
const {database} = action.payload
|
||||||
|
delete database.deleteCode
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
databases: state.databases.map(db => db.links.self === database.links.self ? {...database} : db),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
case 'LOAD_QUERIES': {
|
case 'LOAD_QUERIES': {
|
||||||
return {...state, ...action.payload}
|
return {...state, ...action.payload}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
import AJAX from 'utils/ajax';
|
import AJAX from 'utils/ajax';
|
||||||
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator';
|
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator';
|
||||||
|
|
||||||
export function showDatabases(source) {
|
export const showDatabases = async (source) => {
|
||||||
const query = `SHOW DATABASES`;
|
const query = `SHOW DATABASES`
|
||||||
|
return await proxy({source, query})
|
||||||
|
}
|
||||||
|
|
||||||
return proxy({source, query});
|
export const showRetentionPolicies = async (source, databases) => {
|
||||||
|
let query
|
||||||
|
if (Array.isArray(databases)) {
|
||||||
|
query = databases.map((db) => `SHOW RETENTION POLICIES ON "${db}"`).join(';')
|
||||||
|
} else {
|
||||||
|
query = `SHOW RETENTION POLICIES ON "${databases}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return await proxy({source, query})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showQueries(source, db) {
|
export function showQueries(source, db) {
|
||||||
|
@ -38,17 +48,6 @@ export function showTagValues({source, database, retentionPolicy, measurement, t
|
||||||
return proxy({source, db: database, rp: retentionPolicy, query});
|
return proxy({source, db: database, rp: retentionPolicy, query});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showRetentionPolicies(source, databases) {
|
|
||||||
let query;
|
|
||||||
if (Array.isArray(databases)) {
|
|
||||||
query = databases.map((db) => `SHOW RETENTION POLICIES ON "${db}"`).join(';');
|
|
||||||
} else {
|
|
||||||
query = `SHOW RETENTION POLICIES ON "${databases}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy({source, query});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showShards() {
|
export function showShards() {
|
||||||
return AJAX({
|
return AJAX({
|
||||||
url: `/api/int/v1/show-shards`,
|
url: `/api/int/v1/show-shards`,
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
|
||||||
|
const YesNoButtons = ({onConfirm, onCancel}) => (
|
||||||
|
<div>
|
||||||
|
<button className="btn btn-xs btn-info" onClick={onCancel}>
|
||||||
|
<span className="icon remove"></span>
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-xs btn-success" onClick={onConfirm}>
|
||||||
|
<span className="icon checkmark"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
func,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
YesNoButtons.propTypes = {
|
||||||
|
onConfirm: func.isRequired,
|
||||||
|
onCancel: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default YesNoButtons
|
|
@ -1,21 +1,23 @@
|
||||||
export default function parseShowDatabases(response) {
|
const parseShowDatabases = (response) => {
|
||||||
const results = response.results[0];
|
const results = response.results[0];
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
return {errors: [results.error], databases: []};
|
return {errors: [results.error], databases: []}
|
||||||
}
|
}
|
||||||
|
|
||||||
const series = results.series[0];
|
const series = results.series[0]
|
||||||
if (!series.values) {
|
if (!series.values) {
|
||||||
return {errors: [], databases: []};
|
return {errors: [], databases: []}
|
||||||
}
|
}
|
||||||
|
|
||||||
const databases = series.values.map((s) => {
|
const databases = series.values.map((s) => {
|
||||||
return s[0];
|
return s[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!databases.length) {
|
if (!databases.length) {
|
||||||
alert('No databases were found.'); // eslint-disable-line no-alert
|
alert('No databases were found.'); // eslint-disable-line no-alert
|
||||||
}
|
}
|
||||||
|
|
||||||
return {errors: [], databases};
|
return {errors: [], databases}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default parseShowDatabases
|
||||||
|
|
|
@ -126,4 +126,69 @@
|
||||||
&:first-child {margin-left: 0;}
|
&:first-child {margin-left: 0;}
|
||||||
&:last-child {margin-right: 0;}
|
&:last-child {margin-right: 0;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-table--delete-cell {
|
||||||
|
margin: 0 !important;
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 9px;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 2px;
|
||||||
|
min-width: 110px;
|
||||||
|
|
||||||
|
&:first-child {margin-left: 0;}
|
||||||
|
&:last-child {margin-right: 0;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-manager-header {
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 11px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0px;
|
||||||
|
color: $g15-platinum;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
& .btn {display: none;}
|
||||||
|
&:hover .btn {display: inline-block;}
|
||||||
|
}
|
||||||
|
.db-manager-header-edit {
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 11px;
|
||||||
|
|
||||||
|
> .form-control {
|
||||||
|
width: 300px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 2px 0 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.db-manager {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.db-manager:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-manager-table {
|
||||||
|
background-color: $g4-onyx;
|
||||||
|
padding: 9px 11px;
|
||||||
|
border-radius: $radius-small;
|
||||||
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.panel-body {
|
.panel-body {
|
||||||
padding: 30px;
|
padding: 0px 30px 30px 30px;
|
||||||
}
|
}
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function formatBytes(bytes) {
|
export const formatBytes = (bytes) => {
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
return '0 Bytes';
|
return '0 Bytes';
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,15 @@ export function formatBytes(bytes) {
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatRPDuration(duration) {
|
// Using InfluxDB 1.2+ we should no longer need this formatter.
|
||||||
if (duration === '0') {
|
// Times can now be submitted using multiple units i.e. 1d2h3m
|
||||||
return 'infinite';
|
export const formatRPDuration = (duration) => {
|
||||||
|
if (!duration) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration === '0' || duration === '0s') {
|
||||||
|
return '∞';
|
||||||
}
|
}
|
||||||
|
|
||||||
let adjustedTime = duration;
|
let adjustedTime = duration;
|
||||||
|
@ -38,3 +44,11 @@ export function formatRPDuration(duration) {
|
||||||
|
|
||||||
return adjustedTime;
|
return adjustedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatInfiniteDuration = (duration) => {
|
||||||
|
if (duration === '0' || duration === '0s' || duration === 'INF') {
|
||||||
|
return '∞';
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue