commit
01ec21b483
|
@ -325,6 +325,36 @@ type UsersStore interface {
|
|||
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
|
||||
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.TSDBStatus = &Client{}
|
||||
var _ chronograf.Databases = &Client{}
|
||||
|
||||
// Shared transports for all clients to prevent leaking connections
|
||||
var (
|
||||
|
|
|
@ -75,6 +75,55 @@ func (r *showResults) Users() []chronograf.User {
|
|||
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
|
||||
func (r *showResults) Permissions() chronograf.Permissions {
|
||||
res := []chronograf.Permission{}
|
||||
|
|
|
@ -4,9 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
|
@ -160,7 +158,7 @@ func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
// ReplaceDashboard completely replaces a dashboard
|
||||
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
|
||||
idParam, err := paramID("id", r)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
||||
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
|
||||
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
|
||||
idParam, err := paramID("id", r)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
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.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 out http.Handler
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
client "github.com/influxdata/usage-client/v1"
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
"github.com/tylerb/graceful"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -293,6 +294,7 @@ func openService(ctx context.Context, boltPath, cannedPath string, logger chrono
|
|||
AlertRulesStore: db.AlertsStore,
|
||||
Logger: logger,
|
||||
UseAuth: useAuth,
|
||||
Databases: &influx.Client{Logger: logger},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ type Service struct {
|
|||
TimeSeriesClient TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
Databases chronograf.Databases
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
Databases string `json:"databases"` // URL for the databases contained within this soure
|
||||
}
|
||||
|
||||
type sourceResponse struct {
|
||||
|
@ -43,6 +44,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
|
|||
Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID),
|
||||
Permissions: fmt.Sprintf("%s/%d/permissions", 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",
|
||||
Users: "/chronograf/v1/sources/1/users",
|
||||
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",
|
||||
Users: "/chronograf/v1/sources/1/users",
|
||||
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": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
@ -1912,6 +2235,70 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
|
|
|
@ -3,18 +3,33 @@ import reducer from 'src/admin/reducers/admin'
|
|||
import {
|
||||
addUser,
|
||||
addRole,
|
||||
addDatabase,
|
||||
addRetentionPolicy,
|
||||
syncUser,
|
||||
syncRole,
|
||||
editUser,
|
||||
editRole,
|
||||
editDatabase,
|
||||
editRetentionPolicy,
|
||||
loadRoles,
|
||||
loadPermissions,
|
||||
deleteRole,
|
||||
deleteUser,
|
||||
removeDatabase,
|
||||
removeRetentionPolicy,
|
||||
filterRoles,
|
||||
filterUsers,
|
||||
addDatabaseDeleteCode,
|
||||
removeDatabaseDeleteCode,
|
||||
} from 'src/admin/actions'
|
||||
|
||||
import {
|
||||
NEW_DEFAULT_USER,
|
||||
NEW_DEFAULT_ROLE,
|
||||
NEW_DEFAULT_DATABASE,
|
||||
NEW_EMPTY_RP,
|
||||
} from 'src/admin/constants'
|
||||
|
||||
let state = undefined
|
||||
|
||||
// Users
|
||||
|
@ -58,14 +73,6 @@ const u2 = {
|
|||
links: {self: '/chronograf/v1/sources/1/users/zerocool'},
|
||||
}
|
||||
const users = [u1, u2]
|
||||
const newDefaultUser = {
|
||||
name: '',
|
||||
password: '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
// Roles
|
||||
const r1 = {
|
||||
|
@ -103,20 +110,118 @@ const r2 = {
|
|||
links: {self: '/chronograf/v1/sources/1/roles/l33tus3r'}
|
||||
}
|
||||
const roles = [r1, r2]
|
||||
const newDefaultRole = {
|
||||
name: '',
|
||||
users: [],
|
||||
permissions: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
// Permissions
|
||||
const global = {scope: 'all', allowed: ['p1', 'p2']}
|
||||
const scoped = {scope: 'db1', allowed: ['p1', 'p3']}
|
||||
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('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', () => {
|
||||
state = {
|
||||
users: [
|
||||
|
@ -127,7 +232,7 @@ describe('Admin.Reducers', () => {
|
|||
const actual = reducer(state, addUser())
|
||||
const expected = {
|
||||
users: [
|
||||
{...newDefaultUser, isEditing: true},
|
||||
{...NEW_DEFAULT_USER, isEditing: true},
|
||||
u1,
|
||||
],
|
||||
}
|
||||
|
@ -171,7 +276,7 @@ describe('Admin.Reducers', () => {
|
|||
const actual = reducer(state, addRole())
|
||||
const expected = {
|
||||
roles: [
|
||||
{...newDefaultRole, isEditing: true},
|
||||
{...NEW_DEFAULT_ROLE, isEditing: true},
|
||||
r1,
|
||||
],
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('Formatting helpers', () => {
|
|||
it("returns 'infinite' for a retention policy with a value of '0'", () => {
|
||||
const actual = formatRPDuration('0')
|
||||
|
||||
expect(actual).to.equal('infinite');
|
||||
expect(actual).to.equal('∞');
|
||||
});
|
||||
|
||||
it('correctly formats retention policy durations', () => {
|
||||
|
|
|
@ -2,15 +2,24 @@ import {
|
|||
getUsers as getUsersAJAX,
|
||||
getRoles as getRolesAJAX,
|
||||
getPermissions as getPermissionsAJAX,
|
||||
getDbsAndRps as getDbsAndRpsAJAX,
|
||||
createUser as createUserAJAX,
|
||||
createRole as createRoleAJAX,
|
||||
createDatabase as createDatabaseAJAX,
|
||||
createRetentionPolicy as createRetentionPolicyAJAX,
|
||||
deleteUser as deleteUserAJAX,
|
||||
deleteRole as deleteRoleAJAX,
|
||||
deleteDatabase as deleteDatabaseAJAX,
|
||||
deleteRetentionPolicy as deleteRetentionPolicyAJAX,
|
||||
updateRole as updateRoleAJAX,
|
||||
updateUser as updateUserAJAX,
|
||||
updateRetentionPolicy as updateRetentionPolicyAJAX,
|
||||
} 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 {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 = () => ({
|
||||
type: 'ADD_USER',
|
||||
})
|
||||
|
@ -43,6 +59,17 @@ export const addRole = () => ({
|
|||
type: 'ADD_ROLE',
|
||||
})
|
||||
|
||||
export const addDatabase = () => ({
|
||||
type: 'ADD_DATABASE',
|
||||
})
|
||||
|
||||
export const addRetentionPolicy = (database) => ({
|
||||
type: 'ADD_RETENTION_POLICY',
|
||||
payload: {
|
||||
database,
|
||||
},
|
||||
})
|
||||
|
||||
export const syncUser = (staleUser, syncedUser) => ({
|
||||
type: 'SYNC_USER',
|
||||
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) => ({
|
||||
type: 'EDIT_USER',
|
||||
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) => ({
|
||||
type: 'KILL_QUERY',
|
||||
payload: {
|
||||
|
@ -96,6 +149,7 @@ export const loadQueries = (queries) => ({
|
|||
},
|
||||
})
|
||||
|
||||
// TODO: change to 'removeUser'
|
||||
export const deleteUser = (user) => ({
|
||||
type: 'DELETE_USER',
|
||||
payload: {
|
||||
|
@ -103,6 +157,7 @@ export const deleteUser = (user) => ({
|
|||
},
|
||||
})
|
||||
|
||||
// TODO: change to 'removeRole'
|
||||
export const deleteRole = (role) => ({
|
||||
type: 'DELETE_ROLE',
|
||||
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) => ({
|
||||
type: 'FILTER_USERS',
|
||||
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
|
||||
export const loadUsersAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getUsersAJAX(url)
|
||||
|
@ -140,6 +233,11 @@ export const loadPermissionsAsync = (url) => async (dispatch) => {
|
|||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
// optimistic update
|
||||
dispatch(killQuery(queryID))
|
||||
|
@ -189,6 +322,26 @@ export const deleteUserAsync = (user, addFlashMessage) => (dispatch) => {
|
|||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
|
@ -131,3 +190,18 @@ export const updateUser = async (url, roles, permissions) => {
|
|||
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 {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 RolesTable from 'src/admin/components/RolesTable'
|
||||
import QueriesPage from 'src/admin/containers/QueriesPage'
|
||||
import DatabaseManagerPage from 'src/admin/containers/DatabaseManagerPage'
|
||||
|
||||
const AdminTabs = ({
|
||||
users,
|
||||
|
@ -29,6 +30,10 @@ const AdminTabs = ({
|
|||
onUpdateUserPermissions,
|
||||
}) => {
|
||||
let tabs = [
|
||||
{
|
||||
type: 'DB Management',
|
||||
component: (<DatabaseManagerPage source={source} />),
|
||||
},
|
||||
{
|
||||
type: 'Users',
|
||||
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 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'
|
||||
|
||||
const newDefaultUser = {
|
||||
name: '',
|
||||
password: '',
|
||||
roles: [],
|
||||
permissions: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
const newDefaultRole = {
|
||||
name: '',
|
||||
permissions: [],
|
||||
users: [],
|
||||
links: {self: ''},
|
||||
isNew: true,
|
||||
}
|
||||
import {
|
||||
NEW_DEFAULT_USER,
|
||||
NEW_DEFAULT_ROLE,
|
||||
NEW_DEFAULT_DATABASE,
|
||||
NEW_EMPTY_RP,
|
||||
} from 'src/admin/constants'
|
||||
|
||||
const initialState = {
|
||||
users: null,
|
||||
|
@ -22,6 +12,7 @@ const initialState = {
|
|||
permissions: [],
|
||||
queries: [],
|
||||
queryIDToKill: null,
|
||||
databases: [],
|
||||
}
|
||||
|
||||
export default function admin(state = initialState, action) {
|
||||
|
@ -38,8 +29,12 @@ export default function admin(state = initialState, action) {
|
|||
return {...state, ...action.payload}
|
||||
}
|
||||
|
||||
case 'LOAD_DATABASES': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
|
||||
case 'ADD_USER': {
|
||||
const newUser = {...newDefaultUser, isEditing: true}
|
||||
const newUser = {...NEW_DEFAULT_USER, isEditing: true}
|
||||
return {
|
||||
...state,
|
||||
users: [
|
||||
|
@ -50,7 +45,7 @@ export default function admin(state = initialState, action) {
|
|||
}
|
||||
|
||||
case 'ADD_ROLE': {
|
||||
const newRole = {...newDefaultRole, isEditing: true}
|
||||
const newRole = {...NEW_DEFAULT_ROLE, isEditing: true}
|
||||
return {
|
||||
...state,
|
||||
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': {
|
||||
const {staleUser, syncedUser} = action.payload
|
||||
const newState = {
|
||||
|
@ -76,6 +94,27 @@ export default function admin(state = initialState, action) {
|
|||
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': {
|
||||
const {user, updates} = action.payload
|
||||
const newState = {
|
||||
|
@ -92,6 +131,28 @@ export default function admin(state = initialState, action) {
|
|||
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': {
|
||||
const {user} = action.payload
|
||||
const newState = {
|
||||
|
@ -110,6 +171,48 @@ export default function admin(state = initialState, action) {
|
|||
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': {
|
||||
return {...state, ...action.payload}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import AJAX from 'utils/ajax';
|
||||
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator';
|
||||
|
||||
export function showDatabases(source) {
|
||||
const query = `SHOW DATABASES`;
|
||||
export const showDatabases = async (source) => {
|
||||
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) {
|
||||
|
@ -38,17 +48,6 @@ export function showTagValues({source, database, retentionPolicy, measurement, t
|
|||
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() {
|
||||
return AJAX({
|
||||
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];
|
||||
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) {
|
||||
return {errors: [], databases: []};
|
||||
return {errors: [], databases: []}
|
||||
}
|
||||
|
||||
const databases = series.values.map((s) => {
|
||||
return s[0];
|
||||
return s[0]
|
||||
});
|
||||
|
||||
if (!databases.length) {
|
||||
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;}
|
||||
&: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;
|
||||
}
|
||||
.panel-body {
|
||||
padding: 30px;
|
||||
padding: 0px 30px 30px 30px;
|
||||
}
|
||||
.panel-heading {
|
||||
padding: 0 30px;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function formatBytes(bytes) {
|
||||
export const formatBytes = (bytes) => {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
@ -15,9 +15,15 @@ export function formatBytes(bytes) {
|
|||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export function formatRPDuration(duration) {
|
||||
if (duration === '0') {
|
||||
return 'infinite';
|
||||
// Using InfluxDB 1.2+ we should no longer need this formatter.
|
||||
// Times can now be submitted using multiple units i.e. 1d2h3m
|
||||
export const formatRPDuration = (duration) => {
|
||||
if (!duration) {
|
||||
return
|
||||
}
|
||||
|
||||
if (duration === '0' || duration === '0s') {
|
||||
return '∞';
|
||||
}
|
||||
|
||||
let adjustedTime = duration;
|
||||
|
@ -38,3 +44,11 @@ export function formatRPDuration(duration) {
|
|||
|
||||
return adjustedTime;
|
||||
}
|
||||
|
||||
export const formatInfiniteDuration = (duration) => {
|
||||
if (duration === '0' || duration === '0s' || duration === 'INF') {
|
||||
return '∞';
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue