Merge branch 'master' into chore/merge-master
commit
7db9f4c520
|
@ -1,18 +1,20 @@
|
|||
## v2.0.0-beta.11 [unreleased]
|
||||
## v2.0.0-beta.11 [2020-05-26]
|
||||
|
||||
### Features
|
||||
|
||||
1. [18011](https://github.com/influxdata/influxdb/pull/18011): Integrate UTC dropdown when making custom time range query
|
||||
1. [18040](https://github.com/influxdata/influxdb/pull/18040): Allow for min OR max y-axis visualization settings rather than min AND max
|
||||
1. [17764](https://github.com/influxdata/influxdb/pull/17764): Add CSV to line protocol conversion library
|
||||
1. [18059](https://github.com/influxdata/influxdb/pull/18059): Make the dropdown width adjustable
|
||||
1. [18173](https://github.com/influxdata/influxdb/pull/18173): Add version to /health response
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
1. [18066](https://github.com/influxdata/influxdb/pull/18066): Fixed bug that wasn't persisting timeFormat for Graph + Single Stat selections
|
||||
1. [17959](https://github.com/influxdata/influxdb/pull/17959): Authorizer now exposes full permission set
|
||||
1. [18071](https://github.com/influxdata/influxdb/pull/18071): Fixed issue that was causing variable selections to hydrate all variable values
|
||||
|
||||
### UI Improvements
|
||||
1. [18016](https://github.com/influxdata/influxdb/pull/18016): Remove the fancy scrollbars
|
||||
1. [18171](https://github.com/influxdata/influxdb/pull/18171): Check status now displaying warning if loading a large amount
|
||||
|
||||
## v2.0.0-beta.10 [2020-05-07]
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package authorization
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kit/metric"
|
||||
|
@ -18,10 +17,10 @@ type AuthMetrics struct {
|
|||
|
||||
var _ influxdb.AuthorizationService = (*AuthMetrics)(nil)
|
||||
|
||||
func NewAuthMetrics(reg prometheus.Registerer, s influxdb.AuthorizationService, opts ...MetricsOption) *AuthMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewAuthMetrics(reg prometheus.Registerer, s influxdb.AuthorizationService, opts ...metric.MetricsOption) *AuthMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &AuthMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("token")),
|
||||
rec: metric.New(reg, o.ApplySuffix("token")),
|
||||
authService: s,
|
||||
}
|
||||
}
|
||||
|
@ -59,37 +58,3 @@ func (m *AuthMetrics) DeleteAuthorization(ctx context.Context, id influxdb.ID) e
|
|||
err := m.authService.DeleteAuthorization(ctx, id)
|
||||
return rec(err)
|
||||
}
|
||||
|
||||
// Metrics options
|
||||
type metricOpts struct {
|
||||
serviceSuffix string
|
||||
}
|
||||
|
||||
func defaultOpts() *metricOpts {
|
||||
return &metricOpts{}
|
||||
}
|
||||
|
||||
func (o *metricOpts) applySuffix(prefix string) string {
|
||||
if o.serviceSuffix != "" {
|
||||
return fmt.Sprintf("%s_%s", prefix, o.serviceSuffix)
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
// MetricsOption is an option used by a metric middleware.
|
||||
type MetricsOption func(*metricOpts)
|
||||
|
||||
// WithSuffix returns a metric option that applies a suffix to the service name of the metric.
|
||||
func WithSuffix(suffix string) MetricsOption {
|
||||
return func(opts *metricOpts) {
|
||||
opts.serviceSuffix = suffix
|
||||
}
|
||||
}
|
||||
|
||||
func applyOpts(opts ...MetricsOption) *metricOpts {
|
||||
o := defaultOpts()
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
influxdb "github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kv"
|
||||
jsonp "github.com/influxdata/influxdb/v2/pkg/jsonparser"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -18,6 +19,9 @@ func cmdPing(f *globalFlags, opts genericCLIOpts) *cobra.Command {
|
|||
|
||||
c := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: flags.skipVerify},
|
||||
},
|
||||
}
|
||||
url := flags.Host + "/health"
|
||||
resp, err := c.Get(url)
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/influxdata/influxdb/v2/kit/cli"
|
||||
"github.com/influxdata/influxdb/v2/kit/feature"
|
||||
overrideflagger "github.com/influxdata/influxdb/v2/kit/feature/override"
|
||||
"github.com/influxdata/influxdb/v2/kit/metric"
|
||||
"github.com/influxdata/influxdb/v2/kit/prom"
|
||||
"github.com/influxdata/influxdb/v2/kit/signals"
|
||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
||||
|
@ -619,11 +620,11 @@ func (m *Launcher) run(ctx context.Context) (err error) {
|
|||
|
||||
if m.enableNewMetaStore {
|
||||
ts := tenant.NewService(store)
|
||||
userSvc = tenant.NewUserLogger(m.log.With(zap.String("store", "new")), tenant.NewUserMetrics(m.reg, ts, tenant.WithSuffix("new")))
|
||||
orgSvc = tenant.NewOrgLogger(m.log.With(zap.String("store", "new")), tenant.NewOrgMetrics(m.reg, ts, tenant.WithSuffix("new")))
|
||||
userResourceSvc = tenant.NewURMLogger(m.log.With(zap.String("store", "new")), tenant.NewUrmMetrics(m.reg, ts, tenant.WithSuffix("new")))
|
||||
bucketSvc = tenant.NewBucketLogger(m.log.With(zap.String("store", "new")), tenant.NewBucketMetrics(m.reg, ts, tenant.WithSuffix("new")))
|
||||
passwdsSvc = tenant.NewPasswordLogger(m.log.With(zap.String("store", "new")), tenant.NewPasswordMetrics(m.reg, ts, tenant.WithSuffix("new")))
|
||||
userSvc = tenant.NewUserLogger(m.log.With(zap.String("store", "new")), tenant.NewUserMetrics(m.reg, ts, metric.WithSuffix("new")))
|
||||
orgSvc = tenant.NewOrgLogger(m.log.With(zap.String("store", "new")), tenant.NewOrgMetrics(m.reg, ts, metric.WithSuffix("new")))
|
||||
userResourceSvc = tenant.NewURMLogger(m.log.With(zap.String("store", "new")), tenant.NewUrmMetrics(m.reg, ts, metric.WithSuffix("new")))
|
||||
bucketSvc = tenant.NewBucketLogger(m.log.With(zap.String("store", "new")), tenant.NewBucketMetrics(m.reg, ts, metric.WithSuffix("new")))
|
||||
passwdsSvc = tenant.NewPasswordLogger(m.log.With(zap.String("store", "new")), tenant.NewPasswordMetrics(m.reg, ts, metric.WithSuffix("new")))
|
||||
}
|
||||
|
||||
switch m.secretStore {
|
||||
|
@ -972,7 +973,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
|
|||
{
|
||||
onboardSvc := tenant.NewOnboardService(store, authSvc) // basic service
|
||||
onboardSvc = tenant.NewAuthedOnboardSvc(onboardSvc) // with auth
|
||||
onboardSvc = tenant.NewOnboardingMetrics(m.reg, onboardSvc, tenant.WithSuffix("new")) // with metrics
|
||||
onboardSvc = tenant.NewOnboardingMetrics(m.reg, onboardSvc, metric.WithSuffix("new")) // with metrics
|
||||
onboardSvc = tenant.NewOnboardingLogger(m.log.With(zap.String("handler", "onboard")), onboardSvc) // with logging
|
||||
|
||||
onboardHTTPServer = tenant.NewHTTPOnboardHandler(m.log, onboardSvc)
|
||||
|
|
|
@ -287,7 +287,7 @@ func (s *Service) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilte
|
|||
}
|
||||
}
|
||||
|
||||
return ms, len(ms), s.store.View(ctx, func(tx kv.Tx) error {
|
||||
err := s.store.View(ctx, func(tx kv.Tx) error {
|
||||
// Optimized path, use index.
|
||||
if orgID := filter.OrgID; orgID != nil {
|
||||
// The index performs a prefix search.
|
||||
|
@ -338,6 +338,8 @@ func (s *Service) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilte
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return ms, len(ms), err
|
||||
}
|
||||
|
||||
// Create creates a new mapping.
|
||||
|
|
|
@ -3,11 +3,13 @@ package http
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
platform "github.com/influxdata/influxdb/v2"
|
||||
)
|
||||
|
||||
// HealthHandler returns the status of the process.
|
||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
msg := `{"name":"influxdb", "message":"ready for queries and writes", "status":"pass", "checks":[]}`
|
||||
msg := fmt.Sprintf(`{"name":"influxdb", "message":"ready for queries and writes", "status":"pass", "checks":[], "version": %q, "commit": %q}`, platform.GetBuildInfo().Version, platform.GetBuildInfo().Commit)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, msg)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -11,7 +12,7 @@ func TestHealthHandler(t *testing.T) {
|
|||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
status string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -26,7 +27,7 @@ func TestHealthHandler(t *testing.T) {
|
|||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
body: `{"name":"influxdb", "message":"ready for queries and writes", "status":"pass", "checks":[]}`,
|
||||
status: "pass",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -34,21 +35,37 @@ func TestHealthHandler(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
HealthHandler(tt.w, tt.r)
|
||||
res := tt.w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
contentType := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. HealthHandler() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. HealthHandler() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
if tt.wants.contentType != "" && contentType != tt.wants.contentType {
|
||||
t.Errorf("%q. HealthHandler() = %v, want %v", tt.name, contentType, tt.wants.contentType)
|
||||
}
|
||||
if tt.wants.body != "" {
|
||||
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
|
||||
t.Errorf("%q, HealthHandler(). error unmarshaling json %v", tt.name, err)
|
||||
} else if !eq {
|
||||
t.Errorf("%q. HealthHandler() = ***%s***", tt.name, diff)
|
||||
}
|
||||
var content map[string]interface{}
|
||||
if err := json.Unmarshal(body, &content); err != nil {
|
||||
t.Errorf("%q, HealthHandler(). error unmarshaling json %v", tt.name, err)
|
||||
return
|
||||
}
|
||||
if _, found := content["name"]; !found {
|
||||
t.Errorf("%q. HealthHandler() no name reported", tt.name)
|
||||
}
|
||||
if content["status"] != tt.wants.status {
|
||||
t.Errorf("%q. HealthHandler() status= %v, want %v", tt.name, content["status"], tt.wants.status)
|
||||
}
|
||||
if _, found := content["message"]; !found {
|
||||
t.Errorf("%q. HealthHandler() no message reported", tt.name)
|
||||
}
|
||||
if _, found := content["checks"]; !found {
|
||||
t.Errorf("%q. HealthHandler() no checks reported", tt.name)
|
||||
}
|
||||
if _, found := content["version"]; !found {
|
||||
t.Errorf("%q. HealthHandler() no version reported", tt.name)
|
||||
}
|
||||
if _, found := content["commit"]; !found {
|
||||
t.Errorf("%q. HealthHandler() no commit reported", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10893,6 +10893,10 @@ components:
|
|||
enum:
|
||||
- pass
|
||||
- fail
|
||||
version:
|
||||
type: string
|
||||
commit:
|
||||
type: string
|
||||
Labels:
|
||||
type: array
|
||||
items:
|
||||
|
|
9
id.go
9
id.go
|
@ -25,6 +25,15 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// ErrCorruptID means the ID stored in the Store is corrupt.
|
||||
func ErrCorruptID(err error) *Error {
|
||||
return &Error{
|
||||
Code: EInvalid,
|
||||
Msg: "corrupt ID provided",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ID is a unique identifier.
|
||||
//
|
||||
// Its zero value is not a valid ID.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package tenant
|
||||
package metric
|
||||
|
||||
import "fmt"
|
||||
|
||||
|
@ -10,7 +10,7 @@ func defaultOpts() *metricOpts {
|
|||
return &metricOpts{}
|
||||
}
|
||||
|
||||
func (o *metricOpts) applySuffix(prefix string) string {
|
||||
func (o *metricOpts) ApplySuffix(prefix string) string {
|
||||
if o.serviceSuffix != "" {
|
||||
return fmt.Sprintf("%s_%s", prefix, o.serviceSuffix)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func WithSuffix(suffix string) MetricsOption {
|
|||
}
|
||||
}
|
||||
|
||||
func applyOpts(opts ...MetricsOption) *metricOpts {
|
||||
func ApplyMetricOpts(opts ...MetricsOption) *metricOpts {
|
||||
o := defaultOpts()
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
|
@ -1,11 +1,13 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
||||
ua "github.com/mileusna/useragent"
|
||||
|
@ -130,3 +132,43 @@ func shiftPath(p string) (head, tail string) {
|
|||
}
|
||||
return p[1:i], p[i:]
|
||||
}
|
||||
|
||||
type OrgContext string
|
||||
|
||||
const CtxOrgKey OrgContext = "orgID"
|
||||
|
||||
// ValidResource make sure a resource exists when a sub system needs to be mounted to an api
|
||||
func ValidResource(api *API, lookupOrgByResourceID func(context.Context, influxdb.ID) (influxdb.ID, error)) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
statusW := NewStatusResponseWriter(w)
|
||||
id, err := influxdb.IDFromString(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
api.Err(w, r, influxdb.ErrCorruptID(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
orgID, err := lookupOrgByResourceID(ctx, *id)
|
||||
if err != nil {
|
||||
api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// embed OrgID into context
|
||||
next.ServeHTTP(statusW, r.WithContext(context.WithValue(ctx, CtxOrgKey, orgID)))
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// OrgIDFromContext ....
|
||||
func OrgIDFromContext(ctx context.Context) *influxdb.ID {
|
||||
v := ctx.Value(CtxOrgKey)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
id := v.(influxdb.ID)
|
||||
return &id
|
||||
}
|
||||
|
|
|
@ -311,6 +311,14 @@ func (s *Service) PutLabel(ctx context.Context, l *influxdb.Label) error {
|
|||
})
|
||||
}
|
||||
|
||||
// CreateUserResourceMappingForOrg is a public function that calls createUserResourceMappingForOrg used only for the label service
|
||||
// it can be removed when URMs are removed from the label service
|
||||
func (s *Service) CreateUserResourceMappingForOrg(ctx context.Context, tx Tx, orgID influxdb.ID, resID influxdb.ID, resType influxdb.ResourceType) error {
|
||||
err := s.createUserResourceMappingForOrg(ctx, tx, orgID, resID, resType)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) createUserResourceMappingForOrg(ctx context.Context, tx Tx, orgID influxdb.ID, resID influxdb.ID, resType influxdb.ResourceType) error {
|
||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
||||
defer span.Finish()
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// NotUniqueIDError occurs when attempting to create a Label with an ID that already belongs to another one
|
||||
NotUniqueIDError = &influxdb.Error{
|
||||
Code: influxdb.EConflict,
|
||||
Msg: "ID already exists",
|
||||
}
|
||||
|
||||
// ErrFailureGeneratingID occurs ony when the random number generator
|
||||
// cannot generate an ID in MaxIDGenerationN times.
|
||||
ErrFailureGeneratingID = &influxdb.Error{
|
||||
Code: influxdb.EInternal,
|
||||
Msg: "unable to generate valid id",
|
||||
}
|
||||
|
||||
// ErrLabelNotFound occurs when a label cannot be found by its ID
|
||||
ErrLabelNotFound = &influxdb.Error{
|
||||
Code: influxdb.ENotFound,
|
||||
Msg: "label not found",
|
||||
}
|
||||
)
|
||||
|
||||
// ErrInternalServiceError is used when the error comes from an internal system.
|
||||
func ErrInternalServiceError(err error) *influxdb.Error {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInternal,
|
||||
Err: err,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/pkg/httpc"
|
||||
)
|
||||
|
||||
var _ influxdb.LabelService = (*LabelClientService)(nil)
|
||||
|
||||
type LabelClientService struct {
|
||||
Client *httpc.Client
|
||||
}
|
||||
|
||||
func labelIDPath(id influxdb.ID) string {
|
||||
return path.Join(prefixLabels, id.String())
|
||||
}
|
||||
|
||||
func resourceIDPath(resourceType influxdb.ResourceType, resourceID influxdb.ID, p string) string {
|
||||
return path.Join("/api/v2/", string(resourceType), resourceID.String(), p)
|
||||
}
|
||||
|
||||
// CreateLabel creates a new label.
|
||||
func (s *LabelClientService) CreateLabel(ctx context.Context, l *influxdb.Label) error {
|
||||
var lr labelResponse
|
||||
err := s.Client.
|
||||
PostJSON(l, prefixLabels).
|
||||
DecodeJSON(&lr).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = lr.Label
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindLabelByID returns a single label by ID.
|
||||
func (s *LabelClientService) FindLabelByID(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
|
||||
var lr labelResponse
|
||||
err := s.Client.
|
||||
Get(labelIDPath(id)).
|
||||
DecodeJSON(&lr).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &lr.Label, nil
|
||||
}
|
||||
|
||||
// FindLabels is a client for the find labels response from the server.
|
||||
func (s *LabelClientService) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) ([]*influxdb.Label, error) {
|
||||
params := influxdb.FindOptionParams(opt...)
|
||||
if filter.OrgID != nil {
|
||||
params = append(params, [2]string{"orgID", filter.OrgID.String()})
|
||||
}
|
||||
if filter.Name != "" {
|
||||
params = append(params, [2]string{"name", filter.Name})
|
||||
}
|
||||
|
||||
var lr labelsResponse
|
||||
err := s.Client.
|
||||
Get(prefixLabels).
|
||||
QueryParams(params...).
|
||||
DecodeJSON(&lr).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lr.Labels, nil
|
||||
}
|
||||
|
||||
// FindResourceLabels returns a list of labels, derived from a label mapping filter.
|
||||
func (s *LabelClientService) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
|
||||
if err := filter.Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r labelsResponse
|
||||
err := s.Client.
|
||||
Get(resourceIDPath(filter.ResourceType, filter.ResourceID, "labels")).
|
||||
DecodeJSON(&r).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Labels, nil
|
||||
}
|
||||
|
||||
// UpdateLabel updates a label and returns the updated label.
|
||||
func (s *LabelClientService) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
|
||||
var lr labelResponse
|
||||
err := s.Client.
|
||||
PatchJSON(upd, labelIDPath(id)).
|
||||
DecodeJSON(&lr).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &lr.Label, nil
|
||||
}
|
||||
|
||||
// DeleteLabel removes a label by ID.
|
||||
func (s *LabelClientService) DeleteLabel(ctx context.Context, id influxdb.ID) error {
|
||||
return s.Client.
|
||||
Delete(labelIDPath(id)).
|
||||
Do(ctx)
|
||||
}
|
||||
|
||||
// ******* Label Mappings ******* //
|
||||
|
||||
// CreateLabelMapping will create a labbel mapping
|
||||
func (s *LabelClientService) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
|
||||
if err := m.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
urlPath := resourceIDPath(m.ResourceType, m.ResourceID, "labels")
|
||||
return s.Client.
|
||||
PostJSON(m, urlPath).
|
||||
DecodeJSON(m).
|
||||
Do(ctx)
|
||||
}
|
||||
|
||||
func (s *LabelClientService) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
|
||||
if err := m.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Client.
|
||||
Delete(resourceIDPath(m.ResourceType, m.ResourceID, "labels")).
|
||||
Do(ctx)
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type LabelHandler struct {
|
||||
chi.Router
|
||||
api *kithttp.API
|
||||
log *zap.Logger
|
||||
labelSvc influxdb.LabelService
|
||||
}
|
||||
|
||||
const (
|
||||
prefixLabels = "/api/v2/labels"
|
||||
)
|
||||
|
||||
func (h *LabelHandler) Prefix() string {
|
||||
return prefixLabels
|
||||
}
|
||||
|
||||
func NewHTTPLabelHandler(log *zap.Logger, ls influxdb.LabelService) *LabelHandler {
|
||||
h := &LabelHandler{
|
||||
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
||||
log: log,
|
||||
labelSvc: ls,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(
|
||||
middleware.Recoverer,
|
||||
middleware.RequestID,
|
||||
middleware.RealIP,
|
||||
)
|
||||
|
||||
r.Route("/", func(r chi.Router) {
|
||||
r.Post("/", h.handlePostLabel)
|
||||
r.Get("/", h.handleGetLabels)
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", h.handleGetLabel)
|
||||
r.Patch("/", h.handlePatchLabel)
|
||||
r.Delete("/", h.handleDeleteLabel)
|
||||
})
|
||||
})
|
||||
|
||||
h.Router = r
|
||||
return h
|
||||
}
|
||||
|
||||
type labelResponse struct {
|
||||
Links map[string]string `json:"links"`
|
||||
Label influxdb.Label `json:"label"`
|
||||
}
|
||||
|
||||
func newLabelResponse(l *influxdb.Label) *labelResponse {
|
||||
return &labelResponse{
|
||||
Links: map[string]string{
|
||||
"self": fmt.Sprintf("/api/v2/labels/%s", l.ID),
|
||||
},
|
||||
Label: *l,
|
||||
}
|
||||
}
|
||||
|
||||
type labelsResponse struct {
|
||||
Links map[string]string `json:"links"`
|
||||
Labels []*influxdb.Label `json:"labels"`
|
||||
}
|
||||
|
||||
func newLabelsResponse(ls []*influxdb.Label) *labelsResponse {
|
||||
return &labelsResponse{
|
||||
Links: map[string]string{
|
||||
"self": fmt.Sprintf("/api/v2/labels"),
|
||||
},
|
||||
Labels: ls,
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostLabel is the HTTP handler for the POST /api/v2/labels route.
|
||||
func (h *LabelHandler) handlePostLabel(w http.ResponseWriter, r *http.Request) {
|
||||
var label influxdb.Label
|
||||
if err := h.api.DecodeJSON(r.Body, &label); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := label.Validate(); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.labelSvc.CreateLabel(r.Context(), &label); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
h.log.Debug("Label created", zap.String("label", fmt.Sprint(label)))
|
||||
|
||||
h.api.Respond(w, r, http.StatusCreated, newLabelResponse(&label))
|
||||
}
|
||||
|
||||
// handleGetLabel is the HTTP handler for the GET /api/v2/labels/id route.
|
||||
func (h *LabelHandler) handleGetLabel(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := influxdb.IDFromString(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
l, err := h.labelSvc.FindLabelByID(r.Context(), *id)
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
h.log.Debug("Label retrieved", zap.String("label", fmt.Sprint(l)))
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, newLabelResponse(l))
|
||||
}
|
||||
|
||||
// handleGetLabels is the HTTP handler for the GET /api/v2/labels route.
|
||||
func (h *LabelHandler) handleGetLabels(w http.ResponseWriter, r *http.Request) {
|
||||
var filter influxdb.LabelFilter
|
||||
qp := r.URL.Query()
|
||||
|
||||
if name := qp.Get("name"); name != "" {
|
||||
filter.Name = name
|
||||
}
|
||||
|
||||
if orgID := qp.Get("orgID"); orgID != "" {
|
||||
i, err := influxdb.IDFromString(orgID)
|
||||
if err == nil {
|
||||
filter.OrgID = i
|
||||
}
|
||||
}
|
||||
|
||||
labels, err := h.labelSvc.FindLabels(r.Context(), filter)
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
h.log.Debug("Labels retrived", zap.String("labels", fmt.Sprint(labels)))
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, newLabelsResponse(labels))
|
||||
}
|
||||
|
||||
// handlePatchLabel is the HTTP handler for the PATCH /api/v2/labels route.
|
||||
func (h *LabelHandler) handlePatchLabel(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := influxdb.IDFromString(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
upd := &influxdb.LabelUpdate{}
|
||||
if err := json.NewDecoder(r.Body).Decode(upd); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
l, err := h.labelSvc.UpdateLabel(r.Context(), *id, *upd)
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
h.log.Debug("Label updated", zap.String("label", fmt.Sprint(l)))
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, newLabelResponse(l))
|
||||
}
|
||||
|
||||
// handleDeleteLabel is the HTTP handler for the DELETE /api/v2/labels/:id route.
|
||||
func (h *LabelHandler) handleDeleteLabel(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, err := influxdb.IDFromString(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
if err := h.labelSvc.DeleteLabel(ctx, *id); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
h.log.Debug("Label deleted", zap.String("labelID", fmt.Sprint(id)))
|
||||
|
||||
h.api.Respond(w, r, http.StatusNoContent, nil)
|
||||
}
|
|
@ -0,0 +1,621 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
influxdb "github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/mock"
|
||||
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
"github.com/yudai/gojsondiff"
|
||||
"github.com/yudai/gojsondiff/formatter"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestService_handlePostLabel(t *testing.T) {
|
||||
type fields struct {
|
||||
LabelService influxdb.LabelService
|
||||
}
|
||||
type args struct {
|
||||
label *influxdb.Label
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "create a new label",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
CreateLabelFn: func(ctx context.Context, l *influxdb.Label) error {
|
||||
l.ID = influxdbtesting.MustIDBase16("020f755c3c082000")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
label: &influxdb.Label{
|
||||
Name: "mylabel",
|
||||
OrgID: influxdbtesting.MustIDBase16("020f755c3c082008"),
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusCreated,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/api/v2/labels/020f755c3c082000"
|
||||
},
|
||||
"label": {
|
||||
"id": "020f755c3c082000",
|
||||
"name": "mylabel",
|
||||
"orgID": "020f755c3c082008"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handler := NewHTTPLabelHandler(zaptest.NewLogger(t), tt.fields.LabelService)
|
||||
router := chi.NewRouter()
|
||||
router.Mount(handler.Prefix(), handler)
|
||||
|
||||
l, err := json.Marshal(tt.args.label)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal label: %v", err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(l))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.handlePostLabel(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. handlePostLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. handlePostLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil || tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. handlePostLabel() = ***%v***", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_handleGetLabel(t *testing.T) {
|
||||
type fields struct {
|
||||
LabelService influxdb.LabelService
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "get a label by id",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
FindLabelByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
|
||||
if id == influxdbtesting.MustIDBase16("020f755c3c082000") {
|
||||
return &influxdb.Label{
|
||||
ID: influxdbtesting.MustIDBase16("020f755c3c082000"),
|
||||
Name: "mylabel",
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "020f755c3c082000",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/api/v2/labels/020f755c3c082000"
|
||||
},
|
||||
"label": {
|
||||
"id": "020f755c3c082000",
|
||||
"name": "mylabel",
|
||||
"properties": {
|
||||
"color": "fff000"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
FindLabelByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.ENotFound,
|
||||
Msg: influxdb.ErrLabelNotFound,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "020f755c3c082000",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handler := NewHTTPLabelHandler(zaptest.NewLogger(t), tt.fields.LabelService)
|
||||
router := chi.NewRouter()
|
||||
router.Mount(handler.Prefix(), handler)
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", tt.args.id)
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.handleGetLabel(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. handleGetLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. handleGetLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if tt.wants.body != "" {
|
||||
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
|
||||
t.Errorf("%q, handleGetLabel(). error unmarshaling json %v", tt.name, err)
|
||||
} else if !eq {
|
||||
t.Errorf("%q. handleGetLabel() = ***%s***", tt.name, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_handleGetLabels(t *testing.T) {
|
||||
type fields struct {
|
||||
LabelService influxdb.LabelService
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "get all labels",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
FindLabelsFn: func(ctx context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
|
||||
return []*influxdb.Label{
|
||||
{
|
||||
ID: influxdbtesting.MustIDBase16("0b501e7e557ab1ed"),
|
||||
Name: "hello",
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: influxdbtesting.MustIDBase16("c0175f0077a77005"),
|
||||
Name: "example",
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/api/v2/labels"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": "0b501e7e557ab1ed",
|
||||
"name": "hello",
|
||||
"properties": {
|
||||
"color": "fff000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c0175f0077a77005",
|
||||
"name": "example",
|
||||
"properties": {
|
||||
"color": "fff000"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get all labels when there are none",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
FindLabelsFn: func(ctx context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
|
||||
return []*influxdb.Label{}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/api/v2/labels"
|
||||
},
|
||||
"labels": []
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handler := NewHTTPLabelHandler(zaptest.NewLogger(t), tt.fields.LabelService)
|
||||
router := chi.NewRouter()
|
||||
router.Mount(handler.Prefix(), handler)
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.handleGetLabels(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. handleGetLabels() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. handleGetLabels() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil || tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. handleGetLabels() = ***%v***", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_handlePatchLabel(t *testing.T) {
|
||||
type fields struct {
|
||||
LabelService influxdb.LabelService
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
properties map[string]string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "update label properties",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
UpdateLabelFn: func(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
|
||||
if id == influxdbtesting.MustIDBase16("020f755c3c082000") {
|
||||
l := &influxdb.Label{
|
||||
ID: influxdbtesting.MustIDBase16("020f755c3c082000"),
|
||||
Name: "mylabel",
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range upd.Properties {
|
||||
if v == "" {
|
||||
delete(l.Properties, k)
|
||||
} else {
|
||||
l.Properties[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "020f755c3c082000",
|
||||
properties: map[string]string{
|
||||
"color": "aaabbb",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/api/v2/labels/020f755c3c082000"
|
||||
},
|
||||
"label": {
|
||||
"id": "020f755c3c082000",
|
||||
"name": "mylabel",
|
||||
"properties": {
|
||||
"color": "aaabbb"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label not found",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
UpdateLabelFn: func(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.ENotFound,
|
||||
Msg: influxdb.ErrLabelNotFound,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "020f755c3c082000",
|
||||
properties: map[string]string{
|
||||
"color": "aaabbb",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handler := NewHTTPLabelHandler(zaptest.NewLogger(t), tt.fields.LabelService)
|
||||
router := chi.NewRouter()
|
||||
router.Mount(handler.Prefix(), handler)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
upd := influxdb.LabelUpdate{}
|
||||
if len(tt.args.properties) > 0 {
|
||||
upd.Properties = tt.args.properties
|
||||
}
|
||||
|
||||
l, err := json.Marshal(upd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal label update: %v", err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(l))
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", tt.args.id)
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||
|
||||
handler.handlePatchLabel(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. handlePatchLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. handlePatchLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if tt.wants.body != "" {
|
||||
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
|
||||
t.Errorf("%q, handlePatchLabel(). error unmarshaling json %v", tt.name, err)
|
||||
} else if !eq {
|
||||
t.Errorf("%q. handlePatchLabel() = ***%s***", tt.name, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_handleDeleteLabel(t *testing.T) {
|
||||
type fields struct {
|
||||
LabelService influxdb.LabelService
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "remove a label by id",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
DeleteLabelFn: func(ctx context.Context, id influxdb.ID) error {
|
||||
if id == influxdbtesting.MustIDBase16("020f755c3c082000") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("wrong id")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "020f755c3c082000",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNoContent,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label not found",
|
||||
fields: fields{
|
||||
&mock.LabelService{
|
||||
DeleteLabelFn: func(ctx context.Context, id influxdb.ID) error {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.ENotFound,
|
||||
Msg: influxdb.ErrLabelNotFound,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "020f755c3c082000",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handler := NewHTTPLabelHandler(zaptest.NewLogger(t), tt.fields.LabelService)
|
||||
router := chi.NewRouter()
|
||||
router.Mount(handler.Prefix(), handler)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", tt.args.id)
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||
|
||||
handler.handleDeleteLabel(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. handleDeleteLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. handleDeleteLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if tt.wants.body != "" {
|
||||
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
|
||||
t.Errorf("%q, handleDeleteLabel(). error unmarshaling json %v", tt.name, err)
|
||||
} else if !eq {
|
||||
t.Errorf("%q. handleDeleteLabel() = ***%s***", tt.name, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func jsonEqual(s1, s2 string) (eq bool, diff string, err error) {
|
||||
if s1 == s2 {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if s1 == "" {
|
||||
return false, s2, fmt.Errorf("s1 is empty")
|
||||
}
|
||||
|
||||
if s2 == "" {
|
||||
return false, s1, fmt.Errorf("s2 is empty")
|
||||
}
|
||||
|
||||
var o1 interface{}
|
||||
if err = json.Unmarshal([]byte(s1), &o1); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var o2 interface{}
|
||||
if err = json.Unmarshal([]byte(s2), &o2); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
differ := gojsondiff.New()
|
||||
d, err := differ.Compare([]byte(s1), []byte(s2))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := formatter.AsciiFormatterConfig{}
|
||||
|
||||
formatter := formatter.NewAsciiFormatter(o1, config)
|
||||
diff, err = formatter.Format(d)
|
||||
|
||||
return cmp.Equal(o1, o2), diff, err
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/authorizer"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
)
|
||||
|
||||
var _ influxdb.LabelService = (*AuthedLabelService)(nil)
|
||||
|
||||
type AuthedLabelService struct {
|
||||
s influxdb.LabelService
|
||||
}
|
||||
|
||||
// NewAuthedLabelService constructs an instance of an authorizing label serivce.
|
||||
func NewAuthedLabelService(s influxdb.LabelService) *AuthedLabelService {
|
||||
return &AuthedLabelService{
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
func (s *AuthedLabelService) CreateLabel(ctx context.Context, l *influxdb.Label) error {
|
||||
if _, _, err := authorizer.AuthorizeCreate(ctx, influxdb.LabelsResourceType, l.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.s.CreateLabel(ctx, l)
|
||||
}
|
||||
|
||||
func (s *AuthedLabelService) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) ([]*influxdb.Label, error) {
|
||||
// TODO: we'll likely want to push this operation into the database eventually since fetching the whole list of data
|
||||
// will likely be expensive.
|
||||
ls, err := s.s.FindLabels(ctx, filter, opt...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls, _, err = authorizer.AuthorizeFindLabels(ctx, ls)
|
||||
return ls, err
|
||||
}
|
||||
|
||||
// FindLabelByID checks to see if the authorizer on context has read access to the label id provided.
|
||||
func (s *AuthedLabelService) FindLabelByID(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
|
||||
l, err := s.s.FindLabelByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeRead(ctx, influxdb.LabelsResourceType, id, l.OrgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// FindResourceLabels retrieves all labels belonging to the filtering resource if the authorizer on context has read access to it.
|
||||
// Then it filters the list down to only the labels that are authorized.
|
||||
func (s *AuthedLabelService) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
|
||||
if err := filter.ResourceType.Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// first fetch all labels for this resource
|
||||
ls, err := s.s.FindResourceLabels(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check the permissions for the resource by the org on the context
|
||||
orgID := kithttp.OrgIDFromContext(ctx)
|
||||
if orgID == nil {
|
||||
return nil, errors.New("failed to find orgID on context")
|
||||
}
|
||||
|
||||
if _, _, err := authorizer.AuthorizeRead(ctx, filter.ResourceType, filter.ResourceID, *orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// then filter the labels we got to return only the ones the user is authorized to read
|
||||
ls, _, err = authorizer.AuthorizeFindLabels(ctx, ls)
|
||||
return ls, err
|
||||
}
|
||||
|
||||
// UpdateLabel checks to see if the authorizer on context has write access to the label provided.
|
||||
func (s *AuthedLabelService) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
|
||||
l, err := s.s.FindLabelByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, influxdb.LabelsResourceType, l.ID, l.OrgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.s.UpdateLabel(ctx, id, upd)
|
||||
}
|
||||
|
||||
// DeleteLabel checks to see if the authorizer on context has write access to the label provided.
|
||||
func (s *AuthedLabelService) DeleteLabel(ctx context.Context, id influxdb.ID) error {
|
||||
l, err := s.s.FindLabelByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, influxdb.LabelsResourceType, l.ID, l.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.s.DeleteLabel(ctx, id)
|
||||
}
|
||||
|
||||
// CreateLabelMapping checks to see if the authorizer on context has write access to the label and the resource contained by the label mapping in creation.
|
||||
func (s *AuthedLabelService) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
|
||||
l, err := s.s.FindLabelByID(ctx, m.LabelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, influxdb.LabelsResourceType, m.LabelID, l.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, m.ResourceType, m.ResourceID, l.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.s.CreateLabelMapping(ctx, m)
|
||||
}
|
||||
|
||||
// DeleteLabelMapping checks to see if the authorizer on context has write access to the label and the resource of the label mapping to delete.
|
||||
func (s *AuthedLabelService) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
|
||||
l, err := s.s.FindLabelByID(ctx, m.LabelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, influxdb.LabelsResourceType, m.LabelID, l.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, m.ResourceType, m.ResourceID, l.OrgID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.s.DeleteLabelMapping(ctx, m)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,127 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type LabelLogger struct {
|
||||
logger *zap.Logger
|
||||
labelService influxdb.LabelService
|
||||
}
|
||||
|
||||
func NewLabelLogger(log *zap.Logger, s influxdb.LabelService) *LabelLogger {
|
||||
return &LabelLogger{
|
||||
logger: log,
|
||||
labelService: s,
|
||||
}
|
||||
}
|
||||
|
||||
var _ influxdb.LabelService = (*LabelLogger)(nil)
|
||||
|
||||
func (l *LabelLogger) CreateLabel(ctx context.Context, label *influxdb.Label) (err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to create label", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("label create", dur)
|
||||
}(time.Now())
|
||||
return l.labelService.CreateLabel(ctx, label)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) FindLabelByID(ctx context.Context, id influxdb.ID) (label *influxdb.Label, err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("failed to find label with ID %v", id)
|
||||
l.logger.Debug(msg, zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("label find by ID", dur)
|
||||
}(time.Now())
|
||||
return l.labelService.FindLabelByID(ctx, id)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) (ls []*influxdb.Label, err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to find labels matching the given filter", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("labels find", dur)
|
||||
|
||||
}(time.Now())
|
||||
return l.labelService.FindLabels(ctx, filter, opt...)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) (ls []*influxdb.Label, err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to find resource labels matching the given filter", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("labels for resource find", dur)
|
||||
|
||||
}(time.Now())
|
||||
return l.labelService.FindResourceLabels(ctx, filter)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (lbl *influxdb.Label, err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to update label", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("label update", dur)
|
||||
|
||||
}(time.Now())
|
||||
return l.labelService.UpdateLabel(ctx, id, upd)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) DeleteLabel(ctx context.Context, id influxdb.ID) (err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to delete label", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("label delete", dur)
|
||||
|
||||
}(time.Now())
|
||||
return l.labelService.DeleteLabel(ctx, id)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) (err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to create label mapping", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("label mapping create", dur)
|
||||
|
||||
}(time.Now())
|
||||
return l.labelService.CreateLabelMapping(ctx, m)
|
||||
}
|
||||
|
||||
func (l *LabelLogger) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) (err error) {
|
||||
defer func(start time.Time) {
|
||||
dur := zap.Duration("took", time.Since(start))
|
||||
if err != nil {
|
||||
l.logger.Debug("failed to delete label mapping", zap.Error(err), dur)
|
||||
return
|
||||
}
|
||||
l.logger.Debug("label mapping delete", dur)
|
||||
|
||||
}(time.Now())
|
||||
return l.labelService.DeleteLabelMapping(ctx, m)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package label_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/label"
|
||||
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestLabelLoggingService(t *testing.T) {
|
||||
influxdbtesting.LabelService(initBoltLabelLoggingService, t)
|
||||
}
|
||||
|
||||
func initBoltLabelLoggingService(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
|
||||
svc, s, closer := initBoltLabelService(f, t)
|
||||
return label.NewLabelLogger(zaptest.NewLogger(t), svc), s, closer
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kit/metric"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type LabelMetrics struct {
|
||||
// RED metrics
|
||||
rec *metric.REDClient
|
||||
|
||||
labelService influxdb.LabelService
|
||||
}
|
||||
|
||||
func NewLabelMetrics(reg prometheus.Registerer, s influxdb.LabelService, opts ...metric.MetricsOption) *LabelMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &LabelMetrics{
|
||||
rec: metric.New(reg, o.ApplySuffix("org")),
|
||||
labelService: s,
|
||||
}
|
||||
}
|
||||
|
||||
var _ influxdb.LabelService = (*LabelMetrics)(nil)
|
||||
|
||||
func (m *LabelMetrics) CreateLabel(ctx context.Context, l *influxdb.Label) (err error) {
|
||||
rec := m.rec.Record("create_label")
|
||||
err = m.labelService.CreateLabel(ctx, l)
|
||||
return rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) FindLabelByID(ctx context.Context, id influxdb.ID) (label *influxdb.Label, err error) {
|
||||
rec := m.rec.Record("find_label_by_id")
|
||||
l, err := m.labelService.FindLabelByID(ctx, id)
|
||||
return l, rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) (ls []*influxdb.Label, err error) {
|
||||
rec := m.rec.Record("find_labels")
|
||||
l, err := m.labelService.FindLabels(ctx, filter, opt...)
|
||||
return l, rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) (ls []*influxdb.Label, err error) {
|
||||
rec := m.rec.Record("find_labels_for_resource")
|
||||
l, err := m.labelService.FindResourceLabels(ctx, filter)
|
||||
return l, rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (lbl *influxdb.Label, err error) {
|
||||
rec := m.rec.Record("update_label")
|
||||
l, err := m.labelService.UpdateLabel(ctx, id, upd)
|
||||
return l, rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) DeleteLabel(ctx context.Context, id influxdb.ID) (err error) {
|
||||
rec := m.rec.Record("delete_label")
|
||||
err = m.labelService.DeleteLabel(ctx, id)
|
||||
return rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) CreateLabelMapping(ctx context.Context, lm *influxdb.LabelMapping) (err error) {
|
||||
rec := m.rec.Record("create_label_mapping")
|
||||
err = m.labelService.CreateLabelMapping(ctx, lm)
|
||||
return rec(err)
|
||||
}
|
||||
|
||||
func (m *LabelMetrics) DeleteLabelMapping(ctx context.Context, lm *influxdb.LabelMapping) (err error) {
|
||||
rec := m.rec.Record("delete_label_mapping")
|
||||
err = m.labelService.DeleteLabelMapping(ctx, lm)
|
||||
return rec(err)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package label_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kit/prom"
|
||||
"github.com/influxdata/influxdb/v2/label"
|
||||
"go.uber.org/zap"
|
||||
|
||||
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
)
|
||||
|
||||
func TestLabelMetricsService(t *testing.T) {
|
||||
influxdbtesting.LabelService(initBoltLabelMetricsService, t)
|
||||
}
|
||||
|
||||
func initBoltLabelMetricsService(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
|
||||
svc, s, closer := initBoltLabelService(f, t)
|
||||
reg := prom.NewRegistry(zap.NewNop())
|
||||
return label.NewLabelMetrics(reg, svc), s, closer
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kv"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store *Store
|
||||
urmCreator UserResourceMappingCreator
|
||||
}
|
||||
|
||||
type UserResourceMappingCreator interface {
|
||||
CreateUserResourceMappingForOrg(ctx context.Context, tx kv.Tx, orgID influxdb.ID, resID influxdb.ID, resType influxdb.ResourceType) error
|
||||
}
|
||||
|
||||
func NewService(st *Store, urmCreator UserResourceMappingCreator) influxdb.LabelService {
|
||||
return &Service{
|
||||
store: st,
|
||||
urmCreator: urmCreator, // todo (al) this can be removed once URMs are removed from the Label service
|
||||
}
|
||||
}
|
||||
|
||||
// CreateLabel creates a new label.
|
||||
func (s *Service) CreateLabel(ctx context.Context, l *influxdb.Label) error {
|
||||
if err := l.Validate(); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
l.Name = strings.TrimSpace(l.Name)
|
||||
|
||||
err := s.store.Update(ctx, func(tx kv.Tx) error {
|
||||
if err := uniqueLabelName(ctx, tx, l); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.store.CreateLabel(ctx, tx, l); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.urmCreator.CreateUserResourceMappingForOrg(ctx, tx, l.OrgID, l.ID, influxdb.LabelsResourceType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FindLabelByID finds a label by its ID
|
||||
func (s *Service) FindLabelByID(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
|
||||
var l *influxdb.Label
|
||||
|
||||
err := s.store.View(ctx, func(tx kv.Tx) error {
|
||||
label, e := s.store.GetLabel(ctx, tx, id)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
l = label
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// FindLabels returns a list of labels that match a filter.
|
||||
func (s *Service) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) ([]*influxdb.Label, error) {
|
||||
ls := []*influxdb.Label{}
|
||||
err := s.store.View(ctx, func(tx kv.Tx) error {
|
||||
labels, err := s.store.ListLabels(ctx, tx, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls = labels
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
func (s *Service) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
|
||||
ls := []*influxdb.Label{}
|
||||
if err := s.store.View(ctx, func(tx kv.Tx) error {
|
||||
return s.store.FindResourceLabels(ctx, tx, filter, &ls)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
// UpdateLabel updates a label.
|
||||
func (s *Service) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
|
||||
var label *influxdb.Label
|
||||
err := s.store.Update(ctx, func(tx kv.Tx) error {
|
||||
l, e := s.store.UpdateLabel(ctx, tx, id, upd)
|
||||
if e != nil {
|
||||
return &influxdb.Error{
|
||||
Err: e,
|
||||
}
|
||||
}
|
||||
label = l
|
||||
return nil
|
||||
})
|
||||
|
||||
return label, err
|
||||
}
|
||||
|
||||
// DeleteLabel deletes a label.
|
||||
func (s *Service) DeleteLabel(ctx context.Context, id influxdb.ID) error {
|
||||
err := s.store.Update(ctx, func(tx kv.Tx) error {
|
||||
return s.store.DeleteLabel(ctx, tx, id)
|
||||
})
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//******* Label Mappings *******//
|
||||
|
||||
// CreateLabelMapping creates a new mapping between a resource and a label.
|
||||
func (s *Service) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
|
||||
err := s.store.View(ctx, func(tx kv.Tx) error {
|
||||
if _, err := s.store.GetLabel(ctx, tx, m.LabelID); err != nil {
|
||||
return err
|
||||
}
|
||||
ls := []*influxdb.Label{}
|
||||
err := s.store.FindResourceLabels(ctx, tx, influxdb.LabelMappingFilter{ResourceID: m.ResourceID, ResourceType: m.ResourceType}, &ls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(ls); i++ {
|
||||
if ls[i].ID == m.LabelID {
|
||||
return influxdb.ErrLabelExistsOnResource
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.store.Update(ctx, func(tx kv.Tx) error {
|
||||
return s.store.CreateLabelMapping(ctx, tx, m)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteLabelMapping deletes a label mapping.
|
||||
func (s *Service) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
|
||||
err := s.store.Update(ctx, func(tx kv.Tx) error {
|
||||
return s.store.DeleteLabelMapping(ctx, tx, m)
|
||||
})
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//******* helper functions *******//
|
||||
|
||||
func unique(ctx context.Context, tx kv.Tx, indexBucket, indexKey []byte) error {
|
||||
bucket, err := tx.Bucket(indexBucket)
|
||||
if err != nil {
|
||||
return kv.UnexpectedIndexError(err)
|
||||
}
|
||||
|
||||
_, err = bucket.Get(indexKey)
|
||||
// if not found then this is _unique_.
|
||||
if kv.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// no error means this is not unique
|
||||
if err == nil {
|
||||
return kv.NotUniqueError
|
||||
}
|
||||
|
||||
// any other error is some sort of internal server error
|
||||
return kv.UnexpectedIndexError(err)
|
||||
}
|
||||
|
||||
func uniqueLabelName(ctx context.Context, tx kv.Tx, lbl *influxdb.Label) error {
|
||||
key, err := labelIndexKey(lbl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// labels are unique by `organization:label_name`
|
||||
err = unique(ctx, tx, labelIndex, key)
|
||||
if err == kv.NotUniqueError {
|
||||
return labelAlreadyExistsError(lbl)
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package label_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/bolt"
|
||||
"github.com/influxdata/influxdb/v2/kv"
|
||||
"github.com/influxdata/influxdb/v2/label"
|
||||
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestBoltLabelService(t *testing.T) {
|
||||
influxdbtesting.LabelService(initBoltLabelService, t)
|
||||
}
|
||||
|
||||
func NewTestBoltStore(t *testing.T) (kv.Store, func(), error) {
|
||||
f, err := ioutil.TempFile("", "influxdata-bolt-")
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("unable to open temporary boltdb file")
|
||||
}
|
||||
f.Close()
|
||||
|
||||
path := f.Name()
|
||||
s := bolt.NewKVStore(zaptest.NewLogger(t), path)
|
||||
if err := s.Open(context.Background()); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
close := func() {
|
||||
s.Close()
|
||||
os.Remove(path)
|
||||
}
|
||||
|
||||
return s, close, nil
|
||||
}
|
||||
|
||||
func initBoltLabelService(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
|
||||
s, closeBolt, err := NewTestBoltStore(t)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create new kv store: %v", err)
|
||||
}
|
||||
|
||||
svc, op, closeSvc := initLabelService(s, f, t)
|
||||
return svc, op, func() {
|
||||
closeSvc()
|
||||
closeBolt()
|
||||
}
|
||||
}
|
||||
|
||||
func initLabelService(s kv.Store, f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
|
||||
st, err := label.NewStore(s)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create label store: %v", err)
|
||||
}
|
||||
|
||||
kvSvc := kv.NewService(zaptest.NewLogger(t), s)
|
||||
|
||||
svc := label.NewService(st, kvSvc)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, l := range f.Labels {
|
||||
if err := svc.CreateLabel(ctx, l); err != nil {
|
||||
t.Fatalf("failed to populate labels: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range f.Mappings {
|
||||
if err := svc.CreateLabelMapping(ctx, m); err != nil {
|
||||
t.Fatalf("failed to populate label mappings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return svc, kv.OpPrefix, func() {
|
||||
for _, l := range f.Labels {
|
||||
if err := svc.DeleteLabel(ctx, l.ID); err != nil {
|
||||
t.Logf("failed to remove label: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
||||
"github.com/influxdata/influxdb/v2/kv"
|
||||
"github.com/influxdata/influxdb/v2/snowflake"
|
||||
)
|
||||
|
||||
const MaxIDGenerationN = 100
|
||||
const ReservedIDs = 1000
|
||||
|
||||
var (
|
||||
labelBucket = []byte("labelsv1")
|
||||
labelMappingBucket = []byte("labelmappingsv1")
|
||||
labelIndex = []byte("labelindexv1")
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
kvStore kv.Store
|
||||
IDGenerator influxdb.IDGenerator
|
||||
}
|
||||
|
||||
func NewStore(kvStore kv.Store) (*Store, error) {
|
||||
st := &Store{
|
||||
kvStore: kvStore,
|
||||
IDGenerator: snowflake.NewDefaultIDGenerator(),
|
||||
}
|
||||
return st, st.setup()
|
||||
}
|
||||
|
||||
// View opens up a transaction that will not write to any data. Implementing interfaces
|
||||
// should take care to ensure that all view transactions do not mutate any data.
|
||||
func (s *Store) View(ctx context.Context, fn func(kv.Tx) error) error {
|
||||
return s.kvStore.View(ctx, fn)
|
||||
}
|
||||
|
||||
// Update opens up a transaction that will mutate data.
|
||||
func (s *Store) Update(ctx context.Context, fn func(kv.Tx) error) error {
|
||||
return s.kvStore.Update(ctx, fn)
|
||||
}
|
||||
|
||||
func (s *Store) setup() error {
|
||||
return s.Update(context.Background(), func(tx kv.Tx) error {
|
||||
if _, err := tx.Bucket(labelBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Bucket(labelMappingBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Bucket(labelIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// generateSafeID attempts to create ids for buckets
|
||||
// and orgs that are without backslash, commas, and spaces, BUT ALSO do not already exist.
|
||||
func (s *Store) generateSafeID(ctx context.Context, tx kv.Tx, bucket []byte) (influxdb.ID, error) {
|
||||
for i := 0; i < MaxIDGenerationN; i++ {
|
||||
id := s.IDGenerator.ID()
|
||||
|
||||
// TODO: this is probably unnecessary but for testing we need to keep it in.
|
||||
// After KV is cleaned out we can update the tests and remove this.
|
||||
if id < ReservedIDs {
|
||||
continue
|
||||
}
|
||||
|
||||
err := s.uniqueID(ctx, tx, bucket, id)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
if err == NotUniqueIDError {
|
||||
continue
|
||||
}
|
||||
|
||||
return influxdb.InvalidID(), err
|
||||
}
|
||||
return influxdb.InvalidID(), ErrFailureGeneratingID
|
||||
}
|
||||
|
||||
func (s *Store) uniqueID(ctx context.Context, tx kv.Tx, bucket []byte, id influxdb.ID) error {
|
||||
span, _ := tracing.StartSpanFromContext(ctx)
|
||||
defer span.Finish()
|
||||
|
||||
encodedID, err := id.Encode()
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
b, err := tx.Bucket(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = b.Get(encodedID)
|
||||
if kv.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NotUniqueIDError
|
||||
}
|
|
@ -0,0 +1,501 @@
|
|||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kv"
|
||||
)
|
||||
|
||||
func (s *Store) CreateLabel(ctx context.Context, tx kv.Tx, l *influxdb.Label) error {
|
||||
// if the provided ID is invalid, or already maps to an existing Auth, then generate a new one
|
||||
if !l.ID.Valid() {
|
||||
id, err := s.generateSafeID(ctx, tx, labelBucket)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
l.ID = id
|
||||
} else if err := uniqueID(ctx, tx, l.ID); err != nil {
|
||||
id, err := s.generateSafeID(ctx, tx, labelBucket)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
l.ID = id
|
||||
}
|
||||
|
||||
v, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
encodedID, err := l.ID.Encode()
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
idx, err := tx.Bucket(labelIndex)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key, err := labelIndexKey(l)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := idx.Put([]byte(key), encodedID); err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
b, err := tx.Bucket(labelBucket)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.Put(encodedID, v); err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ListLabels(ctx context.Context, tx kv.Tx, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
|
||||
ls := []*influxdb.Label{}
|
||||
filterFn := filterLabelsFn(filter)
|
||||
err := forEachLabel(ctx, tx, func(l *influxdb.Label) bool {
|
||||
if filterFn(l) {
|
||||
ls = append(ls, l)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetLabel(ctx context.Context, tx kv.Tx, id influxdb.ID) (*influxdb.Label, error) {
|
||||
encodedID, err := id.Encode()
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
b, err := tx.Bucket(labelBucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := b.Get(encodedID)
|
||||
if kv.IsNotFound(err) {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.ENotFound,
|
||||
Msg: influxdb.ErrLabelNotFound,
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var l influxdb.Label
|
||||
if err := json.Unmarshal(v, &l); err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateLabel(ctx context.Context, tx kv.Tx, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
|
||||
label, err := s.GetLabel(ctx, tx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(upd.Properties) > 0 && label.Properties == nil {
|
||||
label.Properties = make(map[string]string)
|
||||
}
|
||||
|
||||
for k, v := range upd.Properties {
|
||||
if v == "" {
|
||||
delete(label.Properties, k)
|
||||
} else {
|
||||
label.Properties[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if upd.Name != "" {
|
||||
upd.Name = strings.TrimSpace(upd.Name)
|
||||
|
||||
idx, err := tx.Bucket(labelIndex)
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key, err := labelIndexKey(label)
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := idx.Delete(key); err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
label.Name = upd.Name
|
||||
if err := uniqueLabelName(ctx, tx, label); err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := label.Validate(); err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
v, err := json.Marshal(label)
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
encodedID, err := label.ID.Encode()
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
idx, err := tx.Bucket(labelIndex)
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key, err := labelIndexKey(label)
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := idx.Put([]byte(key), encodedID); err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
b, err := tx.Bucket(labelBucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := b.Put(encodedID, v); err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return label, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteLabel(ctx context.Context, tx kv.Tx, id influxdb.ID) error {
|
||||
label, err := s.GetLabel(ctx, tx, id)
|
||||
if err != nil {
|
||||
return ErrLabelNotFound
|
||||
}
|
||||
encodedID, idErr := id.Encode()
|
||||
if idErr != nil {
|
||||
return &influxdb.Error{
|
||||
Err: idErr,
|
||||
}
|
||||
}
|
||||
|
||||
b, err := tx.Bucket(labelBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.Delete(encodedID); err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
idx, err := tx.Bucket(labelIndex)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key, err := labelIndexKey(label)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := idx.Delete(key); err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//********* Label Mappings *********//
|
||||
|
||||
func (s *Store) CreateLabelMapping(ctx context.Context, tx kv.Tx, m *influxdb.LabelMapping) error {
|
||||
v, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key, err := labelMappingKey(m)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
idx, err := tx.Bucket(labelMappingBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.Put(key, v); err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) FindResourceLabels(ctx context.Context, tx kv.Tx, filter influxdb.LabelMappingFilter, ls *[]*influxdb.Label) error {
|
||||
if !filter.ResourceID.Valid() {
|
||||
return &influxdb.Error{Code: influxdb.EInvalid, Msg: "filter requires a valid resource id", Err: influxdb.ErrInvalidID}
|
||||
}
|
||||
idx, err := tx.Bucket(labelMappingBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, err := filter.ResourceID.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cur, err := idx.ForwardCursor(prefix, kv.WithCursorPrefix(prefix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, _ := cur.Next(); k != nil; k, _ = cur.Next() {
|
||||
_, id, err := decodeLabelMappingKey(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l, err := s.GetLabel(ctx, tx, id)
|
||||
if l == nil && err != nil {
|
||||
// TODO(jm): return error instead of continuing once orphaned mappings are fixed
|
||||
// (see https://github.com/influxdata/influxdb/issues/11278)
|
||||
continue
|
||||
}
|
||||
|
||||
*ls = append(*ls, l)
|
||||
}
|
||||
|
||||
if err := cur.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cur.Close()
|
||||
}
|
||||
|
||||
func (s *Store) DeleteLabelMapping(ctx context.Context, tx kv.Tx, m *influxdb.LabelMapping) error {
|
||||
key, err := labelMappingKey(m)
|
||||
if err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
idx, err := tx.Bucket(labelMappingBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.Delete(key); err != nil {
|
||||
return &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//********* helper functions *********//
|
||||
|
||||
func labelMappingKey(m *influxdb.LabelMapping) ([]byte, error) {
|
||||
lid, err := m.LabelID.Encode()
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
rid, err := m.ResourceID.Encode()
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
key := make([]byte, influxdb.IDLength+influxdb.IDLength) // len(rid) + len(lid)
|
||||
copy(key, rid)
|
||||
copy(key[len(rid):], lid)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// labelAlreadyExistsError is used when creating a new label with
|
||||
// a name that has already been used. Label names must be unique.
|
||||
func labelAlreadyExistsError(lbl *influxdb.Label) error {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EConflict,
|
||||
Msg: fmt.Sprintf("label with name %s already exists", lbl.Name),
|
||||
}
|
||||
}
|
||||
|
||||
func labelIndexKey(l *influxdb.Label) ([]byte, error) {
|
||||
orgID, err := l.OrgID.Encode()
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
k := make([]byte, influxdb.IDLength+len(l.Name))
|
||||
copy(k, orgID)
|
||||
copy(k[influxdb.IDLength:], []byte(strings.ToLower((l.Name))))
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func filterLabelsFn(filter influxdb.LabelFilter) func(l *influxdb.Label) bool {
|
||||
return func(label *influxdb.Label) bool {
|
||||
return (filter.Name == "" || (strings.EqualFold(filter.Name, label.Name))) &&
|
||||
((filter.OrgID == nil) || (filter.OrgID != nil && *filter.OrgID == label.OrgID))
|
||||
}
|
||||
}
|
||||
|
||||
func decodeLabelMappingKey(key []byte) (resourceID influxdb.ID, labelID influxdb.ID, err error) {
|
||||
if len(key) != 2*influxdb.IDLength {
|
||||
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "malformed label mapping key (please report this error)"}
|
||||
}
|
||||
|
||||
if err := (&resourceID).Decode(key[:influxdb.IDLength]); err != nil {
|
||||
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "bad resource id", Err: influxdb.ErrInvalidID}
|
||||
}
|
||||
|
||||
if err := (&labelID).Decode(key[influxdb.IDLength:]); err != nil {
|
||||
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "bad label id", Err: influxdb.ErrInvalidID}
|
||||
}
|
||||
|
||||
return resourceID, labelID, nil
|
||||
}
|
||||
|
||||
func forEachLabel(ctx context.Context, tx kv.Tx, fn func(*influxdb.Label) bool) error {
|
||||
b, err := tx.Bucket(labelBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cur, err := b.ForwardCursor(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := cur.Next(); k != nil; k, v = cur.Next() {
|
||||
l := &influxdb.Label{}
|
||||
if err := json.Unmarshal(v, l); err != nil {
|
||||
return err
|
||||
}
|
||||
if !fn(l) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := cur.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cur.Close()
|
||||
}
|
||||
|
||||
// uniqueID returns nil if the ID provided is unique, returns an error otherwise
|
||||
func uniqueID(ctx context.Context, tx kv.Tx, id influxdb.ID) error {
|
||||
encodedID, err := id.Encode()
|
||||
if err != nil {
|
||||
return influxdb.ErrInvalidID
|
||||
}
|
||||
|
||||
b, err := tx.Bucket(labelBucket)
|
||||
if err != nil {
|
||||
return ErrInternalServiceError(err)
|
||||
}
|
||||
|
||||
_, err = b.Get(encodedID)
|
||||
// if not found then the ID is unique
|
||||
if kv.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
// no error means this is not unique
|
||||
if err == nil {
|
||||
return kv.NotUniqueError
|
||||
}
|
||||
|
||||
// any other error is some sort of internal server error
|
||||
return kv.UnexpectedIndexError(err)
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
package label_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/inmem"
|
||||
"github.com/influxdata/influxdb/v2/kv"
|
||||
"github.com/influxdata/influxdb/v2/label"
|
||||
)
|
||||
|
||||
func TestLabels(t *testing.T) {
|
||||
s := func() kv.Store {
|
||||
return inmem.NewKVStore()
|
||||
}
|
||||
|
||||
setup := func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
for i := 1; i <= 10; i++ {
|
||||
err := store.CreateLabel(context.Background(), tx, &influxdb.Label{
|
||||
ID: influxdb.ID(i),
|
||||
Name: fmt.Sprintf("labelname%d", i),
|
||||
OrgID: influxdb.ID(i),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupForList := func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
setup(t, store, tx)
|
||||
|
||||
err := store.CreateLabel(context.Background(), tx, &influxdb.Label{
|
||||
ID: influxdb.ID(11),
|
||||
Name: fmt.Sprintf("labelname%d", 11),
|
||||
OrgID: influxdb.ID(5),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
setup func(*testing.T, *label.Store, kv.Tx)
|
||||
update func(*testing.T, *label.Store, kv.Tx)
|
||||
results func(*testing.T, *label.Store, kv.Tx)
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
setup: setup,
|
||||
results: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
labels, err := store.ListLabels(context.Background(), tx, influxdb.LabelFilter{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(labels) != 10 {
|
||||
t.Fatalf("expected 10 labels, got: %d", len(labels))
|
||||
}
|
||||
|
||||
expected := []*influxdb.Label{}
|
||||
for i := 1; i <= 10; i++ {
|
||||
expected = append(expected, &influxdb.Label{
|
||||
ID: influxdb.ID(i),
|
||||
Name: fmt.Sprintf("labelname%d", i),
|
||||
OrgID: influxdb.ID(i),
|
||||
})
|
||||
}
|
||||
if !reflect.DeepEqual(labels, expected) {
|
||||
t.Fatalf("expected identical labels: \n%+v\n%+v", labels, expected)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get",
|
||||
setup: setup,
|
||||
results: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
label, err := store.GetLabel(context.Background(), tx, influxdb.ID(1))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := &influxdb.Label{
|
||||
ID: influxdb.ID(1),
|
||||
Name: "labelname1",
|
||||
OrgID: influxdb.ID(1),
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(label, expected) {
|
||||
t.Fatalf("expected identical label: \n%+v\n%+v", label, expected)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list",
|
||||
setup: setupForList,
|
||||
results: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
// list all
|
||||
labels, err := store.ListLabels(context.Background(), tx, influxdb.LabelFilter{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(labels) != 11 {
|
||||
t.Fatalf("expected 11 labels, got: %d", len(labels))
|
||||
}
|
||||
|
||||
expected := []*influxdb.Label{}
|
||||
for i := 1; i <= 10; i++ {
|
||||
expected = append(expected, &influxdb.Label{
|
||||
ID: influxdb.ID(i),
|
||||
Name: fmt.Sprintf("labelname%d", i),
|
||||
OrgID: influxdb.ID(i),
|
||||
})
|
||||
}
|
||||
expected = append(expected, &influxdb.Label{
|
||||
ID: influxdb.ID(11),
|
||||
Name: fmt.Sprintf("labelname%d", 11),
|
||||
OrgID: influxdb.ID(5),
|
||||
})
|
||||
|
||||
if !reflect.DeepEqual(labels, expected) {
|
||||
t.Fatalf("expected identical labels: \n%+v\n%+v", labels, expected)
|
||||
}
|
||||
|
||||
// filter by name
|
||||
l, err := store.ListLabels(context.Background(), tx, influxdb.LabelFilter{Name: "labelname5"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(l) != 1 {
|
||||
t.Fatalf("expected 1 label, got: %d", len(l))
|
||||
}
|
||||
|
||||
expectedLabel := []*influxdb.Label{&influxdb.Label{
|
||||
ID: influxdb.ID(5),
|
||||
Name: "labelname5",
|
||||
OrgID: influxdb.ID(5),
|
||||
}}
|
||||
if !reflect.DeepEqual(l, expectedLabel) {
|
||||
t.Fatalf("label returned by list did not match expected: \n%+v\n%+v", l, expectedLabel)
|
||||
}
|
||||
|
||||
// filter by org id
|
||||
id := influxdb.ID(5)
|
||||
l, err = store.ListLabels(context.Background(), tx, influxdb.LabelFilter{OrgID: &id})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(l) != 2 {
|
||||
t.Fatalf("expected 2 labels, got: %d", len(l))
|
||||
}
|
||||
|
||||
expectedLabel = []*influxdb.Label{
|
||||
&influxdb.Label{
|
||||
ID: influxdb.ID(5),
|
||||
Name: "labelname5",
|
||||
OrgID: influxdb.ID(5)},
|
||||
{
|
||||
ID: influxdb.ID(11),
|
||||
Name: "labelname11",
|
||||
OrgID: influxdb.ID(5),
|
||||
}}
|
||||
if !reflect.DeepEqual(l, expectedLabel) {
|
||||
t.Fatalf("label returned by list did not match expected: \n%+v\n%+v", l, expectedLabel)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
setup: setup,
|
||||
update: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
upd := influxdb.LabelUpdate{Name: "newName"}
|
||||
updated, err := store.UpdateLabel(context.Background(), tx, influxdb.ID(1), upd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updated.Name != upd.Name {
|
||||
t.Fatalf("expected updated name %s, got: %s", upd.Name, updated.Name)
|
||||
}
|
||||
},
|
||||
results: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
la, err := store.GetLabel(context.Background(), tx, influxdb.ID(1))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if la.Name != "newName" {
|
||||
t.Fatalf("expected update name to be %s, got: %s", "newName", la.Name)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
setup: setup,
|
||||
update: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
err := store.DeleteLabel(context.Background(), tx, influxdb.ID(5))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.DeleteLabel(context.Background(), tx, influxdb.ID(5))
|
||||
if err != label.ErrLabelNotFound {
|
||||
t.Fatal("expected label not found error when deleting bucket that has already been deleted, got: ", err)
|
||||
}
|
||||
},
|
||||
results: func(t *testing.T, store *label.Store, tx kv.Tx) {
|
||||
l, err := store.ListLabels(context.Background(), tx, influxdb.LabelFilter{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(l) != 9 {
|
||||
t.Fatalf("expected 2 labels, got: %d", len(l))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testScenario := range tt {
|
||||
t.Run(testScenario.name, func(t *testing.T) {
|
||||
ts, err := label.NewStore(s())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// setup
|
||||
if testScenario.setup != nil {
|
||||
err := ts.Update(context.Background(), func(tx kv.Tx) error {
|
||||
testScenario.setup(t, ts, tx)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// update
|
||||
if testScenario.update != nil {
|
||||
err := ts.Update(context.Background(), func(tx kv.Tx) error {
|
||||
testScenario.update(t, ts, tx)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// results
|
||||
if testScenario.results != nil {
|
||||
err := ts.View(context.Background(), func(tx kv.Tx) error {
|
||||
testScenario.results(t, ts, tx)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -45,15 +45,6 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// ErrCorruptID the ID stored in the Store is corrupt.
|
||||
func ErrCorruptID(err error) *influxdb.Error {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "corrupt ID provided",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInternalServiceError is used when the error comes from an internal system.
|
||||
func ErrInternalServiceError(err error) *influxdb.Error {
|
||||
return &influxdb.Error{
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
kit "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
)
|
||||
|
||||
type tenantContext string
|
||||
|
||||
const ctxOrgKey tenantContext = "orgID"
|
||||
|
||||
// ValidResource make sure a resource exists when a sub system needs to be mounted to an api
|
||||
func ValidResource(api *kit.API, lookupOrgByResourceID func(context.Context, influxdb.ID) (influxdb.ID, error)) kit.Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
statusW := kit.NewStatusResponseWriter(w)
|
||||
id, err := influxdb.IDFromString(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
api.Err(w, r, ErrCorruptID(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
orgID, err := lookupOrgByResourceID(ctx, *id)
|
||||
if err != nil {
|
||||
api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(statusW, r.WithContext(context.WithValue(ctx, ctxOrgKey, orgID)))
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func orgIDFromContext(ctx context.Context) *influxdb.ID {
|
||||
v := ctx.Value(ctxOrgKey)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
id := v.(influxdb.ID)
|
||||
return &id
|
||||
}
|
|
@ -52,7 +52,7 @@ func NewHTTPBucketHandler(log *zap.Logger, bucketSvc influxdb.BucketService, urm
|
|||
r.Delete("/", svr.handleDeleteBucket)
|
||||
|
||||
// mount embedded resources
|
||||
mountableRouter := r.With(ValidResource(svr.api, svr.lookupOrgByBucketID))
|
||||
mountableRouter := r.With(kithttp.ValidResource(svr.api, svr.lookupOrgByBucketID))
|
||||
mountableRouter.Mount("/members", urmHandler)
|
||||
mountableRouter.Mount("/owners", urmHandler)
|
||||
mountableRouter.Mount("/labels", labelHandler)
|
||||
|
|
|
@ -53,7 +53,7 @@ func NewHTTPOrgHandler(log *zap.Logger, orgService influxdb.OrganizationService,
|
|||
r.Delete("/", svr.handleDeleteOrg)
|
||||
|
||||
// mount embedded resources
|
||||
mountableRouter := r.With(ValidResource(svr.api, svr.lookupOrgByID))
|
||||
mountableRouter := r.With(kithttp.ValidResource(svr.api, svr.lookupOrgByID))
|
||||
mountableRouter.Mount("/members", urm)
|
||||
mountableRouter.Mount("/owners", urm)
|
||||
mountableRouter.Mount("/labels", labelHandler)
|
||||
|
|
|
@ -18,10 +18,10 @@ type BucketMetrics struct {
|
|||
var _ influxdb.BucketService = (*BucketMetrics)(nil)
|
||||
|
||||
// NewBucketMetrics returns a metrics service middleware for the Bucket Service.
|
||||
func NewBucketMetrics(reg prometheus.Registerer, s influxdb.BucketService, opts ...MetricsOption) *BucketMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewBucketMetrics(reg prometheus.Registerer, s influxdb.BucketService, opts ...metric.MetricsOption) *BucketMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &BucketMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("bucket")),
|
||||
rec: metric.New(reg, o.ApplySuffix("bucket")),
|
||||
bucketService: s,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,10 @@ type OnboardingMetrics struct {
|
|||
}
|
||||
|
||||
// NewOnboardingMetrics returns a metrics service middleware for the User Service.
|
||||
func NewOnboardingMetrics(reg prometheus.Registerer, s influxdb.OnboardingService, opts ...MetricsOption) *OnboardingMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewOnboardingMetrics(reg prometheus.Registerer, s influxdb.OnboardingService, opts ...metric.MetricsOption) *OnboardingMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &OnboardingMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("onboard")),
|
||||
rec: metric.New(reg, o.ApplySuffix("onboard")),
|
||||
onboardingService: s,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,10 @@ type OrgMetrics struct {
|
|||
var _ influxdb.OrganizationService = (*OrgMetrics)(nil)
|
||||
|
||||
// NewOrgMetrics returns a metrics service middleware for the Organization Service.
|
||||
func NewOrgMetrics(reg prometheus.Registerer, s influxdb.OrganizationService, opts ...MetricsOption) *OrgMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewOrgMetrics(reg prometheus.Registerer, s influxdb.OrganizationService, opts ...metric.MetricsOption) *OrgMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &OrgMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("org")),
|
||||
rec: metric.New(reg, o.ApplySuffix("org")),
|
||||
orgService: s,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/authorizer"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
)
|
||||
|
||||
type AuthedURMService struct {
|
||||
|
@ -26,8 +27,8 @@ func (s *AuthedURMService) FindUserResourceMappings(ctx context.Context, filter
|
|||
}
|
||||
|
||||
authedUrms := urms[:0]
|
||||
orgID := kithttp.OrgIDFromContext(ctx)
|
||||
for _, urm := range urms {
|
||||
orgID := orgIDFromContext(ctx)
|
||||
if orgID != nil {
|
||||
if _, _, err := authorizer.AuthorizeRead(ctx, urm.ResourceType, urm.ResourceID, *orgID); err != nil {
|
||||
continue
|
||||
|
@ -44,7 +45,7 @@ func (s *AuthedURMService) FindUserResourceMappings(ctx context.Context, filter
|
|||
}
|
||||
|
||||
func (s *AuthedURMService) CreateUserResourceMapping(ctx context.Context, m *influxdb.UserResourceMapping) error {
|
||||
orgID := orgIDFromContext(ctx)
|
||||
orgID := kithttp.OrgIDFromContext(ctx)
|
||||
if orgID != nil {
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, m.ResourceType, m.ResourceID, *orgID); err != nil {
|
||||
return err
|
||||
|
@ -71,7 +72,7 @@ func (s *AuthedURMService) DeleteUserResourceMapping(ctx context.Context, resour
|
|||
|
||||
// There should only be one because resourceID and userID are used to create the primary key for urms
|
||||
for _, urm := range urms {
|
||||
orgID := orgIDFromContext(ctx)
|
||||
orgID := kithttp.OrgIDFromContext(ctx)
|
||||
if orgID != nil {
|
||||
if _, _, err := authorizer.AuthorizeWrite(ctx, urm.ResourceType, urm.ResourceID, *orgID); err != nil {
|
||||
return err
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
influxdbcontext "github.com/influxdata/influxdb/v2/context"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"github.com/influxdata/influxdb/v2/mock"
|
||||
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
)
|
||||
|
@ -105,7 +106,7 @@ func TestURMService_FindUserResourceMappings(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewAuthedURMService(tt.fields.OrgService, tt.fields.UserResourceMappingService)
|
||||
orgID := influxdbtesting.IDPtr(10)
|
||||
ctx := context.WithValue(context.Background(), ctxOrgKey, *orgID)
|
||||
ctx := context.WithValue(context.Background(), kithttp.CtxOrgKey, *orgID)
|
||||
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, tt.args.permissions))
|
||||
|
||||
urms, _, err := s.FindUserResourceMappings(ctx, influxdb.UserResourceMappingFilter{})
|
||||
|
|
|
@ -18,10 +18,10 @@ type UrmMetrics struct {
|
|||
var _ influxdb.UserResourceMappingService = (*UrmMetrics)(nil)
|
||||
|
||||
// NewUrmMetrics returns a metrics service middleware for the User Resource Mapping Service.
|
||||
func NewUrmMetrics(reg prometheus.Registerer, s influxdb.UserResourceMappingService, opts ...MetricsOption) *UrmMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewUrmMetrics(reg prometheus.Registerer, s influxdb.UserResourceMappingService, opts ...metric.MetricsOption) *UrmMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &UrmMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("urm")),
|
||||
rec: metric.New(reg, o.ApplySuffix("urm")),
|
||||
urmService: s,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@ type UserMetrics struct {
|
|||
}
|
||||
|
||||
// NewUserMetrics returns a metrics service middleware for the User Service.
|
||||
func NewUserMetrics(reg prometheus.Registerer, s influxdb.UserService, opts ...MetricsOption) *UserMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewUserMetrics(reg prometheus.Registerer, s influxdb.UserService, opts ...metric.MetricsOption) *UserMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &UserMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("user")),
|
||||
rec: metric.New(reg, o.ApplySuffix("user")),
|
||||
userService: s,
|
||||
}
|
||||
}
|
||||
|
@ -71,10 +71,10 @@ type PasswordMetrics struct {
|
|||
}
|
||||
|
||||
// NewPasswordMetrics returns a metrics service middleware for the Password Service.
|
||||
func NewPasswordMetrics(reg prometheus.Registerer, s influxdb.PasswordsService, opts ...MetricsOption) *PasswordMetrics {
|
||||
o := applyOpts(opts...)
|
||||
func NewPasswordMetrics(reg prometheus.Registerer, s influxdb.PasswordsService, opts ...metric.MetricsOption) *PasswordMetrics {
|
||||
o := metric.ApplyMetricOpts(opts...)
|
||||
return &PasswordMetrics{
|
||||
rec: metric.New(reg, o.applySuffix("password")),
|
||||
rec: metric.New(reg, o.ApplySuffix("password")),
|
||||
pwdService: s,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,14 +82,6 @@ func (s *Service) FindBuckets(ctx context.Context, filter influxdb.BucketFilter,
|
|||
return []*influxdb.Bucket{b}, 1, nil
|
||||
}
|
||||
|
||||
if filter.Name != nil && filter.OrganizationID != nil {
|
||||
b, err := s.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return []*influxdb.Bucket{b}, 1, nil
|
||||
}
|
||||
|
||||
var buckets []*influxdb.Bucket
|
||||
err := s.store.View(ctx, func(tx kv.Tx) error {
|
||||
if filter.OrganizationID == nil && filter.Org != nil {
|
||||
|
@ -100,6 +92,15 @@ func (s *Service) FindBuckets(ctx context.Context, filter influxdb.BucketFilter,
|
|||
filter.OrganizationID = &org.ID
|
||||
}
|
||||
|
||||
if filter.Name != nil && filter.OrganizationID != nil {
|
||||
b, err := s.store.GetBucketByName(ctx, tx, *filter.OrganizationID, *filter.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buckets = []*influxdb.Bucket{b}
|
||||
return nil
|
||||
}
|
||||
|
||||
bs, err := s.store.ListBuckets(ctx, tx, BucketFilter{
|
||||
Name: filter.Name,
|
||||
OrganizationID: filter.OrganizationID,
|
||||
|
|
|
@ -55,3 +55,32 @@ func initBucketService(s kv.Store, f influxdbtesting.BucketFields, t *testing.T)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucketFind(t *testing.T) {
|
||||
s, close, err := NewTestInmemStore(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close()
|
||||
storage, err := tenant.NewStore(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
svc := tenant.NewService(storage)
|
||||
o := &influxdb.Organization{
|
||||
Name: "theorg",
|
||||
}
|
||||
|
||||
if err := svc.CreateOrganization(context.Background(), o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := "thebucket"
|
||||
_, _, err = svc.FindBuckets(context.Background(), influxdb.BucketFilter{
|
||||
Name: &name,
|
||||
Org: &o.Name,
|
||||
})
|
||||
if err.Error() != `bucket "thebucket" not found` {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ func (s *Store) GetOrgByName(ctx context.Context, tx kv.Tx, n string) (*influxdb
|
|||
|
||||
var id influxdb.ID
|
||||
if err := id.Decode(uid); err != nil {
|
||||
return nil, ErrCorruptID(err)
|
||||
return nil, influxdb.ErrCorruptID(err)
|
||||
}
|
||||
return s.GetOrg(ctx, tx, id)
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ func (s *Store) GetUserByName(ctx context.Context, tx kv.Tx, n string) (*influxd
|
|||
|
||||
var id influxdb.ID
|
||||
if err := id.Decode(uid); err != nil {
|
||||
return nil, ErrCorruptID(err)
|
||||
return nil, influxdb.ErrCorruptID(err)
|
||||
}
|
||||
return s.GetUser(ctx, tx, id)
|
||||
}
|
||||
|
|
|
@ -370,10 +370,13 @@ func CreateDBRPMappingV2(
|
|||
}
|
||||
}
|
||||
|
||||
dbrpMappings, _, err := s.FindMany(ctx, influxdb.DBRPMappingFilterV2{})
|
||||
dbrpMappings, n, err := s.FindMany(ctx, influxdb.DBRPMappingFilterV2{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve dbrps: %v", err)
|
||||
}
|
||||
if n != len(tt.wants.dbrpMappings) {
|
||||
t.Errorf("want dbrpMappings count of %d, got %d", len(tt.wants.dbrpMappings), n)
|
||||
}
|
||||
if diff := cmp.Diff(tt.wants.dbrpMappings, dbrpMappings, DBRPMappingCmpOptionsV2...); diff != "" {
|
||||
t.Errorf("dbrpMappings are different -want/+got\ndiff %s", diff)
|
||||
}
|
||||
|
|
|
@ -243,6 +243,7 @@ func CreateLabel(
|
|||
args: args{
|
||||
label: &influxdb.Label{
|
||||
Name: "Tag2",
|
||||
ID: MustIDBase16(labelOneID),
|
||||
OrgID: MustIDBase16(orgOneID),
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
|
@ -940,32 +941,6 @@ func CreateLabelMapping(
|
|||
},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: "duplicate label mappings",
|
||||
// fields: LabelFields{
|
||||
// IDGenerator: mock.NewIDGenerator(labelOneID, t),
|
||||
// Labels: []*influxdb.Label{},
|
||||
// },
|
||||
// args: args{
|
||||
// label: &influxdb.Label{
|
||||
// Name: "Tag2",
|
||||
// Properties: map[string]string{
|
||||
// "color": "fff000",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// wants: wants{
|
||||
// labels: []*influxdb.Label{
|
||||
// {
|
||||
// ID: MustIDBase16(labelOneID),
|
||||
// Name: "Tag2",
|
||||
// Properties: map[string]string{
|
||||
// "color": "fff000",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -36,7 +36,7 @@ export const loadStatuses = (
|
|||
orgID: string,
|
||||
{offset, limit, since, until, filter}: LoadRowsOptions
|
||||
): CancelBox<StatusRow[]> => {
|
||||
const start = since ? Math.round(since / 1000) : '-60d'
|
||||
const start = since ? Math.round(since / 1000) : '-1d'
|
||||
const fluxFilter = filter ? searchExprToFlux(renameTagKeys(filter)) : null
|
||||
|
||||
const query = `
|
||||
|
|
|
@ -54,6 +54,23 @@ import {LIMIT} from 'src/resources/constants'
|
|||
|
||||
type Action = BucketAction | NotifyAction
|
||||
|
||||
export const fetchAllBuckets = async (orgID: string) => {
|
||||
const resp = await api.getBuckets({
|
||||
query: {orgID, limit: LIMIT},
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(resp.data.message)
|
||||
}
|
||||
|
||||
const demoDataBuckets = await fetchDemoDataBuckets()
|
||||
|
||||
return normalize<Bucket, BucketEntities, string[]>(
|
||||
[...resp.data.buckets, ...demoDataBuckets],
|
||||
arrayOfBuckets
|
||||
)
|
||||
}
|
||||
|
||||
export const getBuckets = () => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
|
@ -65,20 +82,7 @@ export const getBuckets = () => async (
|
|||
}
|
||||
const org = getOrg(state)
|
||||
|
||||
const resp = await api.getBuckets({
|
||||
query: {orgID: org.id, limit: LIMIT},
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(resp.data.message)
|
||||
}
|
||||
|
||||
const demoDataBuckets = await fetchDemoDataBuckets()
|
||||
|
||||
const buckets = normalize<Bucket, BucketEntities, string[]>(
|
||||
[...resp.data.buckets, ...demoDataBuckets],
|
||||
arrayOfBuckets
|
||||
)
|
||||
const buckets = await fetchAllBuckets(org.id)
|
||||
|
||||
dispatch(setBuckets(RemoteDataState.Done, buckets))
|
||||
} catch (error) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {createView} from 'src/views/helpers'
|
|||
import {getOrg} from 'src/organizations/selectors'
|
||||
import {toPostCheck, builderToPostCheck} from 'src/checks/utils'
|
||||
import {getAll, getStatus} from 'src/resources/selectors'
|
||||
import {getErrorMessage} from 'src/utils/api'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
|
@ -151,17 +152,14 @@ export const createCheckFromTimeMachine = () => async (
|
|||
dispatch: Dispatch<Action | SendToTimeMachineAction>,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const rename = 'Please rename the check before saving'
|
||||
try {
|
||||
const state = getState()
|
||||
const check = builderToPostCheck(state)
|
||||
const resp = await api.postCheck({data: check})
|
||||
if (resp.status !== 201) {
|
||||
if (resp.data.code.includes('conflict')) {
|
||||
throw new Error(
|
||||
`A check named ${
|
||||
check.name
|
||||
} already exists. Please rename the check before saving`
|
||||
)
|
||||
throw new Error(`A check named ${check.name} already exists. ${rename}`)
|
||||
}
|
||||
throw new Error(resp.data.message)
|
||||
}
|
||||
|
@ -178,11 +176,14 @@ export const createCheckFromTimeMachine = () => async (
|
|||
dispatch(resetAlertBuilder())
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(notify(copy.createCheckFailed(error.message)))
|
||||
reportError(error, {
|
||||
context: {state: getState()},
|
||||
name: 'saveCheckFromTimeMachine function',
|
||||
})
|
||||
const message = getErrorMessage(error)
|
||||
dispatch(notify(copy.createCheckFailed(message)))
|
||||
if (!message.includes(rename)) {
|
||||
reportError(error, {
|
||||
context: {state: getState()},
|
||||
name: 'saveCheckFromTimeMachine function',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,10 @@ export const builderToPostCheck = (state: AppState) => {
|
|||
if (check.type === 'deadman') {
|
||||
return toDeadManPostCheck(alertBuilder, check)
|
||||
}
|
||||
|
||||
if (check.type === 'custom') {
|
||||
return {...check, status: check.activeStatus}
|
||||
}
|
||||
}
|
||||
|
||||
const toDeadManPostCheck = (
|
||||
|
|
|
@ -86,7 +86,7 @@ $event-table--field-margin: $ix-marg-b;
|
|||
}
|
||||
|
||||
to {
|
||||
opacity: 0.8;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Libraries
|
||||
import React, {useLayoutEffect, FC} from 'react'
|
||||
import React, {useLayoutEffect, FC, useEffect, useState} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {AutoSizer, InfiniteLoader, List} from 'react-virtualized'
|
||||
|
||||
// Components
|
||||
|
@ -9,6 +10,9 @@ import LoadingRow from 'src/eventViewer/components/LoadingRow'
|
|||
import FooterRow from 'src/eventViewer/components/FooterRow'
|
||||
import ErrorRow from 'src/eventViewer/components/ErrorRow'
|
||||
|
||||
// Actions
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
loadNextRows,
|
||||
|
@ -19,17 +23,42 @@ import {
|
|||
import {EventViewerChildProps, Fields} from 'src/eventViewer/types'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
type Props = EventViewerChildProps & {
|
||||
// Constants
|
||||
import {checkStatusLoading} from 'src/shared/copy/notifications'
|
||||
|
||||
type DispatchProps = {
|
||||
notify: typeof notifyAction
|
||||
}
|
||||
|
||||
type OwnProps = {
|
||||
fields: Fields
|
||||
}
|
||||
|
||||
const EventTable: FC<Props> = ({state, dispatch, loadRows, fields}) => {
|
||||
type Props = EventViewerChildProps & DispatchProps & OwnProps
|
||||
|
||||
const EventTable: FC<Props> = ({state, dispatch, loadRows, fields, notify}) => {
|
||||
const rowCount = getRowCount(state)
|
||||
|
||||
const isRowLoaded = ({index}) => !!state.rows[index]
|
||||
|
||||
const isRowLoadedBoolean = !!state.rows[0]
|
||||
|
||||
const loadMoreRows = () => loadNextRows(state, dispatch, loadRows)
|
||||
|
||||
const [isLongRunningQuery, setIsLongRunningQuery] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLongRunningQuery(true)
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isLongRunningQuery && !isRowLoadedBoolean) {
|
||||
notify(checkStatusLoading)
|
||||
}
|
||||
}, [isLongRunningQuery, isRowLoaded])
|
||||
|
||||
const rowRenderer = ({key, index, style}) => {
|
||||
const isLastRow = index === state.rows.length
|
||||
|
||||
|
@ -103,4 +132,11 @@ const EventTable: FC<Props> = ({state, dispatch, loadRows, fields}) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default EventTable
|
||||
const mdtp: DispatchProps = {
|
||||
notify: notifyAction,
|
||||
}
|
||||
|
||||
export default connect<{}, DispatchProps, OwnProps>(
|
||||
null,
|
||||
mdtp
|
||||
)(EventTable)
|
||||
|
|
|
@ -28,7 +28,7 @@ const LoadingRow: FC<Props> = ({index, style}) => {
|
|||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${PLACEHOLDER_HEIGHT}px`,
|
||||
animationDelay: `${(index % 5) / 2}s`,
|
||||
animationDelay: `${((index % 5) / 2) * 100}ms`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,8 @@ import {getAllVariables, asAssignment} from 'src/variables/selectors'
|
|||
import {buildVarsOption} from 'src/variables/utils/buildVarsOption'
|
||||
import {runQuery} from 'src/shared/apis/query'
|
||||
import {parseResponse as parse} from 'src/shared/parsing/flux/response'
|
||||
import {getOrg} from 'src/organizations/selectors'
|
||||
import {fetchAllBuckets} from 'src/buckets/actions/thunks'
|
||||
|
||||
import {store} from 'src/index'
|
||||
|
||||
|
@ -109,8 +111,6 @@ const queryTagValues = async (orgID, bucket, tag) => {
|
|||
export class LSPServer {
|
||||
private server: WASMServer
|
||||
private messageID: number = 0
|
||||
private buckets: string[] = []
|
||||
private orgID: string = ''
|
||||
private documentVersions: {[key: string]: number} = {}
|
||||
public store: Store<AppState & LocalStorage>
|
||||
|
||||
|
@ -125,7 +125,8 @@ export class LSPServer {
|
|||
|
||||
getTagKeys = async bucket => {
|
||||
try {
|
||||
const response = await queryTagKeys(this.orgID, bucket)
|
||||
const org = getOrg(this.store.getState())
|
||||
const response = await queryTagKeys(org.id, bucket)
|
||||
return parseQueryResponse(response)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
@ -135,7 +136,8 @@ export class LSPServer {
|
|||
|
||||
getTagValues = async (bucket, tag) => {
|
||||
try {
|
||||
const response = await queryTagValues(this.orgID, bucket, tag)
|
||||
const org = getOrg(this.store.getState())
|
||||
const response = await queryTagValues(org.id, bucket, tag)
|
||||
return parseQueryResponse(response)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
@ -143,13 +145,22 @@ export class LSPServer {
|
|||
}
|
||||
}
|
||||
|
||||
getBuckets = () => {
|
||||
return Promise.resolve(this.buckets)
|
||||
getBuckets = async () => {
|
||||
try {
|
||||
const org = getOrg(this.store.getState())
|
||||
const buckets = await fetchAllBuckets(org.id)
|
||||
|
||||
return Object.values(buckets.entities.buckets).map(b => b.name)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
getMeasurements = async (bucket: string) => {
|
||||
try {
|
||||
const response = await queryMeasurements(this.orgID, bucket)
|
||||
const org = getOrg(this.store.getState())
|
||||
const response = await queryMeasurements(org.id, bucket)
|
||||
return parseQueryResponse(response)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
@ -157,14 +168,6 @@ export class LSPServer {
|
|||
}
|
||||
}
|
||||
|
||||
updateBuckets(buckets: string[]) {
|
||||
this.buckets = buckets
|
||||
}
|
||||
|
||||
setOrg(orgID: string) {
|
||||
this.orgID = orgID
|
||||
}
|
||||
|
||||
initialize() {
|
||||
return this.send(initialize(this.currentMessageID))
|
||||
}
|
||||
|
|
|
@ -29,13 +29,23 @@ export const loadLocalStorage = (): LocalStorage => {
|
|||
}
|
||||
}
|
||||
|
||||
const isValidJSONString = errorString => {
|
||||
try {
|
||||
JSON.parse(errorString)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const saveToLocalStorage = (state: LocalStorage): void => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
'state',
|
||||
JSON.stringify(normalizeSetLocalStorage(state))
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Unable to save state to local storage: ', JSON.parse(err))
|
||||
} catch (error) {
|
||||
const errorMessage = isValidJSONString(error) ? JSON.parse(error) : error
|
||||
console.error('Unable to save state to local storage: ', errorMessage)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {FC, useContext} from 'react'
|
||||
import React, {FC, useContext, useCallback, useMemo} from 'react'
|
||||
|
||||
import {Page} from '@influxdata/clockface'
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
|
@ -14,25 +14,34 @@ import {SubmitQueryButton} from 'src/timeMachine/components/SubmitQueryButton'
|
|||
|
||||
const FULL_WIDTH = true
|
||||
|
||||
const Header: FC = () => {
|
||||
const {id} = useContext(NotebookContext)
|
||||
const {timeContext, addTimeContext, updateTimeContext} = useContext(
|
||||
TimeContext
|
||||
)
|
||||
const ConnectedTimeZoneDropdown = React.memo(() => {
|
||||
const {timeZone, onSetTimeZone} = useContext(AppSettingContext)
|
||||
|
||||
if (!timeContext.hasOwnProperty(id)) {
|
||||
addTimeContext(id)
|
||||
return null
|
||||
return <TimeZoneDropdown timeZone={timeZone} onSetTimeZone={onSetTimeZone} />
|
||||
})
|
||||
|
||||
const ConnectedTimeRangeDropdown = ({context, update}) => {
|
||||
const {range} = context
|
||||
|
||||
const updateRange = range => {
|
||||
update({
|
||||
range,
|
||||
})
|
||||
}
|
||||
|
||||
const {refresh, range} = timeContext[id]
|
||||
return useMemo(() => {
|
||||
return <TimeRangeDropdown timeRange={range} onSetTimeRange={updateRange} />
|
||||
}, [range])
|
||||
}
|
||||
|
||||
function updateRefresh(interval: number) {
|
||||
const ConnectedAutoRefreshDropdown = ({context, update}) => {
|
||||
const {refresh} = context
|
||||
|
||||
const updateRefresh = (interval: number) => {
|
||||
const status =
|
||||
interval === 0 ? AutoRefreshStatus.Paused : AutoRefreshStatus.Active
|
||||
|
||||
updateTimeContext(id, {
|
||||
update({
|
||||
refresh: {
|
||||
status,
|
||||
interval,
|
||||
|
@ -40,13 +49,46 @@ const Header: FC = () => {
|
|||
} as TimeBlock)
|
||||
}
|
||||
|
||||
function updateRange(range) {
|
||||
updateTimeContext(id, {
|
||||
...timeContext[id],
|
||||
range,
|
||||
})
|
||||
return useMemo(
|
||||
() => (
|
||||
<AutoRefreshDropdown
|
||||
selected={refresh}
|
||||
onChoose={updateRefresh}
|
||||
showManualRefresh={false}
|
||||
/>
|
||||
),
|
||||
[refresh]
|
||||
)
|
||||
}
|
||||
|
||||
const EnsureTimeContextExists: FC = () => {
|
||||
const {id} = useContext(NotebookContext)
|
||||
const {timeContext, addTimeContext, updateTimeContext} = useContext(
|
||||
TimeContext
|
||||
)
|
||||
|
||||
const update = useCallback(
|
||||
data => {
|
||||
updateTimeContext(id, data)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
if (!timeContext.hasOwnProperty(id)) {
|
||||
addTimeContext(id)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectedTimeZoneDropdown />
|
||||
<ConnectedTimeRangeDropdown context={timeContext[id]} update={update} />
|
||||
<ConnectedAutoRefreshDropdown context={timeContext[id]} update={update} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Header: FC = () => {
|
||||
function submit() {} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
|
||||
return (
|
||||
|
@ -60,16 +102,7 @@ const Header: FC = () => {
|
|||
</Page.ControlBarLeft>
|
||||
<Page.ControlBarRight>
|
||||
<div className="notebook-header--buttons">
|
||||
<TimeZoneDropdown
|
||||
timeZone={timeZone}
|
||||
onSetTimeZone={onSetTimeZone}
|
||||
/>
|
||||
<TimeRangeDropdown timeRange={range} onSetTimeRange={updateRange} />
|
||||
<AutoRefreshDropdown
|
||||
selected={refresh}
|
||||
onChoose={updateRefresh}
|
||||
showManualRefresh={false}
|
||||
/>
|
||||
<EnsureTimeContextExists />
|
||||
<SubmitQueryButton
|
||||
submitButtonDisabled={false}
|
||||
queryStatus={RemoteDataState.NotStarted}
|
||||
|
@ -82,8 +115,6 @@ const Header: FC = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export {Header}
|
||||
|
||||
export default () => (
|
||||
<TimeProvider>
|
||||
<AppSettingProvider>
|
||||
|
@ -91,3 +122,5 @@ export default () => (
|
|||
</AppSettingProvider>
|
||||
</TimeProvider>
|
||||
)
|
||||
|
||||
export {Header}
|
||||
|
|
|
@ -4,7 +4,6 @@ import {Page} from '@influxdata/clockface'
|
|||
import {NotebookProvider} from 'src/notebooks/context/notebook'
|
||||
import Header from 'src/notebooks/components/Header'
|
||||
import PipeList from 'src/notebooks/components/PipeList'
|
||||
import NotebookPanel from 'src/notebooks/components/panel/NotebookPanel'
|
||||
|
||||
// NOTE: uncommon, but using this to scope the project
|
||||
// within the page and not bleed it's dependancies outside
|
||||
|
@ -24,5 +23,4 @@ const NotebookPage: FC = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export {NotebookPanel}
|
||||
export default NotebookPage
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {FC, createElement} from 'react'
|
||||
import {FC, createElement, useMemo} from 'react'
|
||||
|
||||
import {PIPE_DEFINITIONS, PipeProp} from 'src/notebooks'
|
||||
|
||||
|
@ -10,7 +10,10 @@ const Pipe: FC<PipeProp> = props => {
|
|||
return null
|
||||
}
|
||||
|
||||
return createElement(PIPE_DEFINITIONS[data.type].component, props)
|
||||
return useMemo(
|
||||
() => createElement(PIPE_DEFINITIONS[data.type].component, props),
|
||||
[props.data]
|
||||
)
|
||||
}
|
||||
|
||||
export default Pipe
|
||||
|
|
|
@ -1,30 +1,46 @@
|
|||
import React, {FC, useContext, createElement} from 'react'
|
||||
import React, {FC, useContext, useCallback, createElement, useMemo} from 'react'
|
||||
import {PipeContextProps, PipeData} from 'src/notebooks'
|
||||
import Pipe from 'src/notebooks/components/Pipe'
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
import NotebookPanel from 'src/notebooks/components/panel/NotebookPanel'
|
||||
|
||||
const PipeList: FC = () => {
|
||||
const {id, pipes, updatePipe} = useContext(NotebookContext)
|
||||
const _pipes = pipes.map((pipe, index) => {
|
||||
const panel: FC<PipeContextProps> = props => {
|
||||
interface NotebookPipeProps {
|
||||
index: number
|
||||
data: PipeData
|
||||
onUpdate: (index: number, pipe: PipeData) => void
|
||||
}
|
||||
|
||||
const NotebookPipe: FC<NotebookPipeProps> = ({index, data, onUpdate}) => {
|
||||
const panel: FC<PipeContextProps> = useMemo(
|
||||
() => props => {
|
||||
const _props = {
|
||||
...props,
|
||||
index,
|
||||
}
|
||||
|
||||
return createElement(NotebookPanel, _props)
|
||||
}
|
||||
const onUpdate = (data: PipeData) => {
|
||||
updatePipe(index, data)
|
||||
}
|
||||
},
|
||||
[index]
|
||||
)
|
||||
|
||||
const _onUpdate = (data: PipeData) => {
|
||||
onUpdate(index, data)
|
||||
}
|
||||
|
||||
return <Pipe data={data} onUpdate={_onUpdate} Context={panel} />
|
||||
}
|
||||
|
||||
const PipeList: FC = () => {
|
||||
const {id, pipes, updatePipe} = useContext(NotebookContext)
|
||||
const update = useCallback(updatePipe, [id])
|
||||
|
||||
const _pipes = pipes.map((_, index) => {
|
||||
return (
|
||||
<Pipe
|
||||
<NotebookPipe
|
||||
key={`pipe-${id}-${index}`}
|
||||
data={pipe}
|
||||
onUpdate={onUpdate}
|
||||
Context={panel}
|
||||
index={index}
|
||||
data={pipes[index]}
|
||||
onUpdate={update}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Libraries
|
||||
import React, {FC, useContext} from 'react'
|
||||
import React, {FC, useContext, useCallback, ReactNode} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components
|
||||
|
@ -9,26 +9,73 @@ import {
|
|||
AlignItems,
|
||||
JustifyContent,
|
||||
} from '@influxdata/clockface'
|
||||
import {PipeContextProps} from 'src/notebooks'
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
import RemovePanelButton from 'src/notebooks/components/panel/RemovePanelButton'
|
||||
import PanelVisibilityToggle from 'src/notebooks/components/panel/PanelVisibilityToggle'
|
||||
import MovePanelButton from 'src/notebooks/components/panel/MovePanelButton'
|
||||
import NotebookPanelTitle from 'src/notebooks/components/panel/NotebookPanelTitle'
|
||||
|
||||
// Types
|
||||
import {PipeContextProps} from 'src/notebooks'
|
||||
|
||||
// Contexts
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
|
||||
export interface Props extends PipeContextProps {
|
||||
index: number
|
||||
}
|
||||
|
||||
const NotebookPanel: FC<Props> = ({index, children}) => {
|
||||
const {pipes, removePipe, movePipe, meta} = useContext(NotebookContext)
|
||||
export interface HeaderProps {
|
||||
index: number
|
||||
controls?: ReactNode
|
||||
}
|
||||
|
||||
const NotebookPanelHeader: FC<HeaderProps> = ({index, controls}) => {
|
||||
const {pipes, removePipe, movePipe} = useContext(NotebookContext)
|
||||
const canBeMovedUp = index > 0
|
||||
const canBeMovedDown = index < pipes.length - 1
|
||||
const canBeRemoved = index !== 0
|
||||
|
||||
const moveUp = canBeMovedUp ? () => movePipe(index, index - 1) : null
|
||||
const moveDown = canBeMovedDown ? () => movePipe(index, index + 1) : null
|
||||
const remove = canBeRemoved ? () => removePipe(index) : null
|
||||
const moveUp = useCallback(
|
||||
canBeMovedUp ? () => movePipe(index, index - 1) : null,
|
||||
[index, pipes]
|
||||
)
|
||||
const moveDown = useCallback(
|
||||
canBeMovedDown ? () => movePipe(index, index + 1) : null,
|
||||
[index, pipes]
|
||||
)
|
||||
const remove = useCallback(canBeRemoved ? () => removePipe(index) : null, [
|
||||
index,
|
||||
pipes,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="notebook-panel--header">
|
||||
<FlexBox
|
||||
className="notebook-panel--header-left"
|
||||
alignItems={AlignItems.Center}
|
||||
margin={ComponentSize.Small}
|
||||
justifyContent={JustifyContent.FlexStart}
|
||||
>
|
||||
<NotebookPanelTitle index={index} />
|
||||
</FlexBox>
|
||||
<FlexBox
|
||||
className="notebook-panel--header-right"
|
||||
alignItems={AlignItems.Center}
|
||||
margin={ComponentSize.Small}
|
||||
justifyContent={JustifyContent.FlexEnd}
|
||||
>
|
||||
{controls}
|
||||
<MovePanelButton direction="up" onClick={moveUp} />
|
||||
<MovePanelButton direction="down" onClick={moveDown} />
|
||||
<PanelVisibilityToggle index={index} />
|
||||
<RemovePanelButton onRemove={remove} />
|
||||
</FlexBox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NotebookPanel: FC<Props> = ({index, children, controls}) => {
|
||||
const {meta} = useContext(NotebookContext)
|
||||
|
||||
const isVisible = meta[index].visible
|
||||
|
||||
|
@ -39,28 +86,8 @@ const NotebookPanel: FC<Props> = ({index, children}) => {
|
|||
|
||||
return (
|
||||
<div className={panelClassName}>
|
||||
<div className="notebook-panel--header">
|
||||
<FlexBox
|
||||
className="notebook-panel--header-left"
|
||||
alignItems={AlignItems.Center}
|
||||
margin={ComponentSize.Small}
|
||||
justifyContent={JustifyContent.FlexStart}
|
||||
>
|
||||
<NotebookPanelTitle index={index} />
|
||||
</FlexBox>
|
||||
<FlexBox
|
||||
className="notebook-panel--header-right"
|
||||
alignItems={AlignItems.Center}
|
||||
margin={ComponentSize.Small}
|
||||
justifyContent={JustifyContent.FlexEnd}
|
||||
>
|
||||
<MovePanelButton direction="up" onClick={moveUp} />
|
||||
<MovePanelButton direction="down" onClick={moveDown} />
|
||||
<PanelVisibilityToggle index={index} />
|
||||
<RemovePanelButton onRemove={remove} />
|
||||
</FlexBox>
|
||||
</div>
|
||||
<div className="notebook-panel--body">{isVisible && children}</div>
|
||||
<NotebookPanelHeader index={index} controls={controls} />
|
||||
<div className="notebook-panel--body">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,22 +30,20 @@ export const AppSettingContext = React.createContext<AppSettingContextType>(
|
|||
DEFAULT_CONTEXT
|
||||
)
|
||||
|
||||
export const AppSettingProvider: FC<Props> = ({
|
||||
timeZone,
|
||||
onSetTimeZone,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<AppSettingContext.Provider
|
||||
value={{
|
||||
timeZone,
|
||||
onSetTimeZone,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppSettingContext.Provider>
|
||||
)
|
||||
}
|
||||
export const AppSettingProvider: FC<Props> = React.memo(
|
||||
({timeZone, onSetTimeZone, children}) => {
|
||||
return (
|
||||
<AppSettingContext.Provider
|
||||
value={{
|
||||
timeZone,
|
||||
onSetTimeZone,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppSettingContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const mstp = (state: AppState): StateProps => {
|
||||
return {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {FC, useState} from 'react'
|
||||
import React, {FC, useState, useCallback} from 'react'
|
||||
import {PipeData} from 'src/notebooks'
|
||||
|
||||
export interface PipeMeta {
|
||||
|
@ -49,68 +49,86 @@ export const NotebookProvider: FC = ({children}) => {
|
|||
const [pipes, setPipes] = useState(DEFAULT_CONTEXT.pipes)
|
||||
const [meta, setMeta] = useState(DEFAULT_CONTEXT.meta)
|
||||
|
||||
function addPipe(pipe: PipeData) {
|
||||
const add = data => {
|
||||
return pipes => {
|
||||
pipes.push(data)
|
||||
const _setPipes = useCallback(setPipes, [id])
|
||||
const _setMeta = useCallback(setMeta, [id])
|
||||
|
||||
const addPipe = useCallback(
|
||||
(pipe: PipeData) => {
|
||||
const add = data => {
|
||||
return pipes => {
|
||||
pipes.push(data)
|
||||
return pipes.slice()
|
||||
}
|
||||
}
|
||||
_setPipes(add(pipe))
|
||||
_setMeta(
|
||||
add({
|
||||
title: `Notebook_${++GENERATOR_INDEX}`,
|
||||
visible: true,
|
||||
})
|
||||
)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const updatePipe = useCallback(
|
||||
(idx: number, pipe: PipeData) => {
|
||||
_setPipes(pipes => {
|
||||
pipes[idx] = {
|
||||
...pipes[idx],
|
||||
...pipe,
|
||||
}
|
||||
return pipes.slice()
|
||||
})
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const updateMeta = useCallback(
|
||||
(idx: number, pipe: PipeMeta) => {
|
||||
_setMeta(pipes => {
|
||||
pipes[idx] = {
|
||||
...pipes[idx],
|
||||
...pipe,
|
||||
}
|
||||
return pipes.slice()
|
||||
})
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const movePipe = useCallback(
|
||||
(currentIdx: number, newIdx: number) => {
|
||||
const move = list => {
|
||||
const idx = ((newIdx % list.length) + list.length) % list.length
|
||||
|
||||
if (idx === currentIdx) {
|
||||
return list
|
||||
}
|
||||
|
||||
const pipe = list.splice(currentIdx, 1)
|
||||
|
||||
list.splice(idx, 0, pipe[0])
|
||||
|
||||
return list.slice()
|
||||
}
|
||||
_setPipes(move)
|
||||
_setMeta(move)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const removePipe = useCallback(
|
||||
(idx: number) => {
|
||||
const remove = pipes => {
|
||||
pipes.splice(idx, 1)
|
||||
return pipes.slice()
|
||||
}
|
||||
}
|
||||
setPipes(add(pipe))
|
||||
setMeta(
|
||||
add({
|
||||
title: `Notebook_${++GENERATOR_INDEX}`,
|
||||
visible: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function updatePipe(idx: number, pipe: PipeData) {
|
||||
setPipes(pipes => {
|
||||
pipes[idx] = {
|
||||
...pipes[idx],
|
||||
...pipe,
|
||||
}
|
||||
return pipes.slice()
|
||||
})
|
||||
}
|
||||
|
||||
function updateMeta(idx: number, pipe: PipeMeta) {
|
||||
setMeta(pipes => {
|
||||
pipes[idx] = {
|
||||
...pipes[idx],
|
||||
...pipe,
|
||||
}
|
||||
return pipes.slice()
|
||||
})
|
||||
}
|
||||
|
||||
function movePipe(currentIdx: number, newIdx: number) {
|
||||
const move = list => {
|
||||
const idx = ((newIdx % list.length) + list.length) % list.length
|
||||
|
||||
if (idx === currentIdx) {
|
||||
return list
|
||||
}
|
||||
|
||||
const pipe = list.splice(currentIdx, 1)
|
||||
|
||||
list.splice(idx, 0, pipe[0])
|
||||
|
||||
return list.slice()
|
||||
}
|
||||
setPipes(move)
|
||||
setMeta(move)
|
||||
}
|
||||
|
||||
function removePipe(idx: number) {
|
||||
const remove = pipes => {
|
||||
pipes.splice(idx, 1)
|
||||
return pipes.slice()
|
||||
}
|
||||
setPipes(remove)
|
||||
setMeta(remove)
|
||||
}
|
||||
_setPipes(remove)
|
||||
_setMeta(remove)
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
return (
|
||||
<NotebookContext.Provider
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {FC, useState} from 'react'
|
||||
import React, {FC, useState, useCallback} from 'react'
|
||||
import {AutoRefresh, TimeRange} from 'src/types'
|
||||
import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges'
|
||||
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
|
||||
|
@ -36,7 +36,7 @@ export const TimeContext = React.createContext<TimeContext>(DEFAULT_CONTEXT)
|
|||
export const TimeProvider: FC = ({children}) => {
|
||||
const [timeContext, setTimeContext] = useState({})
|
||||
|
||||
function addTimeContext(id: string, block?: TimeBlock) {
|
||||
const addTimeContext = useCallback((id: string, block?: TimeBlock) => {
|
||||
setTimeContext(ranges => {
|
||||
if (ranges.hasOwnProperty(id)) {
|
||||
throw new Error(
|
||||
|
@ -50,9 +50,9 @@ export const TimeProvider: FC = ({children}) => {
|
|||
[id]: {...(block || DEFAULT_STATE)},
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
function updateTimeContext(id: string, block: TimeBlock) {
|
||||
const updateTimeContext = useCallback((id: string, block: TimeBlock) => {
|
||||
setTimeContext(ranges => {
|
||||
return {
|
||||
...ranges,
|
||||
|
@ -62,9 +62,9 @@ export const TimeProvider: FC = ({children}) => {
|
|||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
function removeTimeContext(id: string) {
|
||||
const removeTimeContext = useCallback((id: string) => {
|
||||
setTimeContext(ranges => {
|
||||
if (!ranges.hasOwnProperty(id)) {
|
||||
throw new Error(`TimeContext[${id}] doesn't exist`)
|
||||
|
@ -73,7 +73,7 @@ export const TimeProvider: FC = ({children}) => {
|
|||
delete ranges[id]
|
||||
return {...ranges}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TimeContext.Provider
|
||||
|
|
|
@ -2,6 +2,7 @@ import {FunctionComponent, ComponentClass, ReactNode} from 'react'
|
|||
|
||||
export interface PipeContextProps {
|
||||
children?: ReactNode
|
||||
controls?: ReactNode
|
||||
}
|
||||
|
||||
export type PipeData = any
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import {register} from 'src/notebooks'
|
||||
import View from './view'
|
||||
import './style.scss'
|
||||
|
||||
register({
|
||||
type: 'query',
|
||||
component: View,
|
||||
button: 'Custom Script',
|
||||
initial: {
|
||||
activeQuery: 0,
|
||||
queries: [
|
||||
{
|
||||
text: '',
|
||||
editMode: 'advanced',
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
tags: [],
|
||||
functions: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
.notebook-query {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React, {FC, useMemo} from 'react'
|
||||
import {PipeProp} from 'src/notebooks'
|
||||
import FluxMonacoEditor from 'src/shared/components/FluxMonacoEditor'
|
||||
|
||||
const Query: FC<PipeProp> = ({data, onUpdate, Context}) => {
|
||||
const {queries, activeQuery} = data
|
||||
const query = queries[activeQuery]
|
||||
|
||||
function updateText(text) {
|
||||
const _queries = queries.slice()
|
||||
_queries[activeQuery] = {
|
||||
...queries[activeQuery],
|
||||
text,
|
||||
}
|
||||
|
||||
onUpdate({queries: _queries})
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Context>
|
||||
<FluxMonacoEditor
|
||||
script={query.text}
|
||||
onChangeScript={updateText}
|
||||
onSubmitScript={() => {}}
|
||||
/>
|
||||
</Context>
|
||||
),
|
||||
[query.text]
|
||||
)
|
||||
}
|
||||
|
||||
export default Query
|
|
@ -0,0 +1,38 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import {SelectGroup} from '@influxdata/clockface'
|
||||
|
||||
// Types
|
||||
import {MarkdownMode} from './'
|
||||
|
||||
interface Props {
|
||||
mode: MarkdownMode
|
||||
onToggleMode: (mode: MarkdownMode) => void
|
||||
}
|
||||
|
||||
const MarkdownModeToggle: FC<Props> = ({mode, onToggleMode}) => {
|
||||
return (
|
||||
<SelectGroup>
|
||||
<SelectGroup.Option
|
||||
active={mode === 'edit'}
|
||||
onClick={onToggleMode}
|
||||
value="edit"
|
||||
id="edit"
|
||||
>
|
||||
Edit
|
||||
</SelectGroup.Option>
|
||||
<SelectGroup.Option
|
||||
active={mode === 'preview'}
|
||||
onClick={onToggleMode}
|
||||
value="preview"
|
||||
id="preview"
|
||||
>
|
||||
Preview
|
||||
</SelectGroup.Option>
|
||||
</SelectGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownModeToggle
|
|
@ -0,0 +1,41 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Types
|
||||
import {PipeProp} from 'src/notebooks'
|
||||
import {MarkdownMode} from './'
|
||||
|
||||
// Components
|
||||
import MarkdownModeToggle from './MarkdownModeToggle'
|
||||
import MarkdownPanelEditor from './MarkdownPanelEditor'
|
||||
import {MarkdownRenderer} from 'src/shared/components/views/MarkdownRenderer'
|
||||
|
||||
const MarkdownPanel: FC<PipeProp> = ({data, Context, onUpdate}) => {
|
||||
const handleToggleMode = (mode: MarkdownMode): void => {
|
||||
onUpdate({mode})
|
||||
}
|
||||
|
||||
const controls = (
|
||||
<MarkdownModeToggle mode={data.mode} onToggleMode={handleToggleMode} />
|
||||
)
|
||||
|
||||
const handleChange = (text: string): void => {
|
||||
onUpdate({text})
|
||||
}
|
||||
|
||||
let panelContents = (
|
||||
<MarkdownPanelEditor text={data.text} onChange={handleChange} />
|
||||
)
|
||||
|
||||
if (data.mode === 'preview') {
|
||||
panelContents = (
|
||||
<div className="notebook-panel--markdown markdown-format">
|
||||
<MarkdownRenderer text={data.text} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Context controls={controls}>{panelContents}</Context>
|
||||
}
|
||||
|
||||
export default MarkdownPanel
|
|
@ -0,0 +1,24 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
onChange: (text: string) => void
|
||||
}
|
||||
|
||||
const MarkdownPanelEditor: FC<Props> = ({text, onChange}) => {
|
||||
const handleTextAreaChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
onChange(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className="notebook-panel--markdown-editor"
|
||||
value={text}
|
||||
onChange={handleTextAreaChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownPanelEditor
|
|
@ -0,0 +1,15 @@
|
|||
import {register} from 'src/notebooks'
|
||||
import MarkdownPanel from './MarkdownPanel'
|
||||
import './style.scss'
|
||||
|
||||
export type MarkdownMode = 'edit' | 'preview'
|
||||
|
||||
register({
|
||||
type: 'markdown',
|
||||
component: MarkdownPanel,
|
||||
button: 'Markdown',
|
||||
initial: () => ({
|
||||
text: 'Content',
|
||||
mode: 'edit',
|
||||
}),
|
||||
})
|
|
@ -0,0 +1,45 @@
|
|||
@import "@influxdata/clockface/dist/variables.scss";
|
||||
|
||||
$notebook-panel--bg: mix($g1-raven, $g2-kevlar, 50%);
|
||||
|
||||
.notebook-panel--markdown,
|
||||
.notebook-panel--markdown-editor {
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
padding: $cf-marg-b;
|
||||
border-radius: $cf-radius - 1px;
|
||||
border-width: $cf-border;
|
||||
border-style: solid;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.notebook-panel--markdown {
|
||||
border-color: $g1-raven;
|
||||
|
||||
.notebook-panel:hover & {
|
||||
border-color: $notebook-panel--bg;
|
||||
}
|
||||
}
|
||||
|
||||
.notebook-panel--markdown-editor {
|
||||
padding: $cf-marg-b;
|
||||
background-color: $g1-raven;
|
||||
border-color: $cf-input-border--default;
|
||||
color: $cf-input-text--default;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
transition: $cf-input--transition;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: $cf-input-text--hover;
|
||||
border-color: $cf-input-border--hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: $cf-input-text--focused;
|
||||
border-color: $cf-input-border--focused;
|
||||
box-shadow: $cf-input--box-shadow;
|
||||
}
|
||||
}
|
|
@ -91,7 +91,7 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border;
|
|||
font-weight: $cf-font-weight--medium;
|
||||
transition: color 0.25s ease, background-color 0.25s ease;
|
||||
outline: none;
|
||||
width: 228px;
|
||||
width: 350px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
@ -141,7 +141,6 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border;
|
|||
border-radius: 0 0 $cf-radius $cf-radius;
|
||||
padding: $cf-marg-b;
|
||||
padding-top: 0;
|
||||
// flex: 1 0 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -157,50 +156,12 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border;
|
|||
padding-top: 0;
|
||||
}
|
||||
|
||||
.notebook-panel--body .flux-editor {
|
||||
.notebook-panel--body .flux-editor--monaco {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
min-height: 320px;
|
||||
|
||||
// Special styling for markdown panels
|
||||
.notebook-panel--markdown,
|
||||
.notebook-panel--markdown-edit {
|
||||
font-size: 14px;
|
||||
padding: $cf-marg-b;
|
||||
border-radius: $cf-radius - 1px;
|
||||
border-width: $cf-border;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.notebook-panel--markdown {
|
||||
border-color: $notebook-panel--bg;
|
||||
}
|
||||
|
||||
.notebook-panel--markdown-edit {
|
||||
padding: $cf-marg-b;
|
||||
background-color: $g1-raven;
|
||||
border-color: $cf-input-border--default;
|
||||
color: $cf-input-text--default;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
transition: $cf-input--transition;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: $cf-input-text--hover;
|
||||
border-color: $cf-input-border--hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: $cf-input-text--focused;
|
||||
border-color: $cf-input-border--focused;
|
||||
box-shadow: $cf-input--box-shadow;
|
||||
.react-monaco-editor-container {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,6 @@ class OrgNavigation extends PureComponent<Props> {
|
|||
id: 'members-quartz',
|
||||
cloudOnly: true,
|
||||
href: `${CLOUD_URL}/organizations/${orgID}${CLOUD_USERS_PATH}`,
|
||||
featureFlag: 'multiUser',
|
||||
},
|
||||
{
|
||||
text: 'About',
|
||||
|
|
|
@ -26,7 +26,6 @@ import {MeState} from 'src/shared/reducers/me'
|
|||
// Selectors
|
||||
import {getOrg} from 'src/organizations/selectors'
|
||||
import {getNavItemActivation} from '../utils'
|
||||
import {FeatureFlag} from 'src/shared/utils/featureFlag'
|
||||
|
||||
interface StateProps {
|
||||
org: Organization
|
||||
|
@ -78,19 +77,17 @@ const UserWidget: FC<Props> = ({
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
<FeatureFlag name="multiUser" equals={true}>
|
||||
<TreeNav.UserItem
|
||||
id="users"
|
||||
label="Users"
|
||||
testID="user-nav-item-users"
|
||||
linkElement={className => (
|
||||
<a
|
||||
className={className}
|
||||
href={`${CLOUD_URL}/organizations/${org.id}${CLOUD_USERS_PATH}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
<TreeNav.UserItem
|
||||
id="users"
|
||||
label="Users"
|
||||
testID="user-nav-item-users"
|
||||
linkElement={className => (
|
||||
<a
|
||||
className={className}
|
||||
href={`${CLOUD_URL}/organizations/${org.id}${CLOUD_USERS_PATH}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TreeNav.UserItem
|
||||
id="about"
|
||||
label="About"
|
||||
|
|
|
@ -71,7 +71,10 @@ const EventMarker: FC<Props> = ({xScale, xDomain, events, xFormatter}) => {
|
|||
/>
|
||||
</div>
|
||||
{tooltipVisible && trigger.current && (
|
||||
<BoxTooltip triggerRect={triggerRect} maxWidth={500}>
|
||||
<BoxTooltip
|
||||
triggerRect={triggerRect}
|
||||
maxWidth={formattedEvents[0].message.length * 50}
|
||||
>
|
||||
<EventMarkerTooltip events={formattedEvents} />
|
||||
</BoxTooltip>
|
||||
)}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
// Libraries
|
||||
import {FC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {AppState, Bucket, ResourceType} from 'src/types'
|
||||
import {getAll} from 'src/resources/selectors'
|
||||
import {getOrg} from 'src/organizations/selectors'
|
||||
|
||||
import loadServer from 'src/external/monaco.flux.server'
|
||||
|
||||
const FluxBucketProvider: FC<{}> = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const mstp = (state: AppState): {} => {
|
||||
const buckets = getAll<Bucket>(state, ResourceType.Buckets)
|
||||
const org = getOrg(state)
|
||||
|
||||
loadServer().then(server => {
|
||||
server.updateBuckets(buckets.map(b => b.name))
|
||||
server.setOrg(org.id || '')
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export default connect<{}, {}>(
|
||||
mstp,
|
||||
null
|
||||
)(FluxBucketProvider)
|
|
@ -4,8 +4,6 @@ import {ProtocolToMonacoConverter} from 'monaco-languageclient/lib/monaco-conver
|
|||
|
||||
// Components
|
||||
import MonacoEditor from 'react-monaco-editor'
|
||||
import FluxBucketProvider from 'src/shared/components/FluxBucketProvider'
|
||||
import GetResources from 'src/resources/components/GetResources'
|
||||
|
||||
// Utils
|
||||
import FLUXLANGID from 'src/external/monaco.flux.syntax'
|
||||
|
@ -16,7 +14,7 @@ import {isFlagEnabled} from 'src/shared/utils/featureFlag'
|
|||
|
||||
// Types
|
||||
import {OnChangeScript} from 'src/types/flux'
|
||||
import {EditorType, ResourceType} from 'src/types'
|
||||
import {EditorType} from 'src/types'
|
||||
|
||||
import './FluxMonacoEditor.scss'
|
||||
import {editor as monacoEditor} from 'monaco-editor'
|
||||
|
@ -103,9 +101,6 @@ const FluxEditorMonaco: FC<Props> = ({
|
|||
|
||||
return (
|
||||
<div className="flux-editor--monaco" data-testid="flux-editor">
|
||||
<GetResources resources={[ResourceType.Buckets]}>
|
||||
<FluxBucketProvider />
|
||||
</GetResources>
|
||||
<MonacoEditor
|
||||
language={FLUXLANGID}
|
||||
theme={THEME_NAME}
|
||||
|
|
|
@ -93,6 +93,11 @@ export const SigninError: Notification = {
|
|||
message: `Could not sign in`,
|
||||
}
|
||||
|
||||
export const checkStatusLoading: Notification = {
|
||||
...defaultSuccessNotification,
|
||||
message: `Currently loading checks`,
|
||||
}
|
||||
|
||||
export const QuickstartScraperCreationSuccess: Notification = {
|
||||
...defaultSuccessNotification,
|
||||
message: `The InfluxDB Scraper has been configured for ${QUICKSTART_SCRAPER_TARGET_URL}`,
|
||||
|
|
|
@ -23,7 +23,6 @@ export const CLOUD_FLAGS = {
|
|||
downloadCellCSV: false,
|
||||
fluxParser: false,
|
||||
matchingNotificationRules: false,
|
||||
multiUser: true,
|
||||
notebooks: false,
|
||||
telegrafEditor: false,
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ interface StateProps {
|
|||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onSetActiveQueryText: typeof setActiveQueryText
|
||||
onSubmitQueries: typeof saveAndExecuteQueries
|
||||
onSetActiveQueryText: typeof setActiveQueryText | ((text: string) => void)
|
||||
onSubmitQueries: typeof saveAndExecuteQueries | (() => void)
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps
|
||||
|
@ -160,6 +160,8 @@ const TimeMachineFluxEditor: FC<Props> = ({
|
|||
)
|
||||
}
|
||||
|
||||
export {TimeMachineFluxEditor}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const activeQueryText = getActiveQuery(state).text
|
||||
const {activeTab} = getActiveTimeMachine(state)
|
||||
|
|
|
@ -433,6 +433,7 @@ export const selectValue = (variableID: string, selected: string) => async (
|
|||
|
||||
await dispatch(selectValueInState(contextID, variableID, selected))
|
||||
// only hydrate the changedVariable
|
||||
dispatch(hydrateChangedVariable(variableID))
|
||||
dispatch(hydrateVariables(true))
|
||||
// dispatch(hydrateChangedVariable(variableID))
|
||||
dispatch(updateQueryVars({[variable.name]: selected}))
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class VariableDropdown extends PureComponent<Props> {
|
|||
const longestItemWidth = Math.floor(
|
||||
values.reduce(function(a, b) {
|
||||
return a.length > b.length ? a : b
|
||||
}, '').length * 8.5
|
||||
}, '').length * 8.75
|
||||
)
|
||||
|
||||
const widthLength = Math.max(140, longestItemWidth)
|
||||
|
|
|
@ -121,16 +121,16 @@ describe('hydrate vars', () => {
|
|||
// }
|
||||
expect(
|
||||
actual.filter(v => v.id === 'a')[0].arguments.values.results
|
||||
).toEqual([])
|
||||
).toBeFalsy()
|
||||
expect(
|
||||
actual.filter(v => v.id === 'b')[0].arguments.values.results
|
||||
).toEqual([])
|
||||
).toBeFalsy()
|
||||
expect(
|
||||
actual.filter(v => v.id === 'c')[0].arguments.values.results
|
||||
).toEqual([])
|
||||
).toBeFalsy()
|
||||
expect(
|
||||
actual.filter(v => v.id === 'd')[0].arguments.values.results
|
||||
).toEqual([])
|
||||
).toBeFalsy()
|
||||
|
||||
expect(
|
||||
actual.filter(v => v.id === 'e')[0].arguments.values.results
|
||||
|
@ -325,7 +325,7 @@ describe('hydrate vars', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('findSubgraph', () => {
|
||||
xdescribe('findSubgraph', () => {
|
||||
test('should return the update variable with all associated parents', async () => {
|
||||
const variableGraph = await createVariableGraph(defaultVariables)
|
||||
const actual = await findSubgraph(variableGraph, [defaultVariable])
|
||||
|
|
|
@ -128,8 +128,9 @@ export const findSubgraph = (
|
|||
const subgraph: Set<VariableNode> = new Set()
|
||||
// use an ID array to reduce the chance of reference errors
|
||||
const varIDs = variables.map(v => v.id)
|
||||
// TODO: uncomment this when variable hydration is resolved
|
||||
// create an array of IDs to reference later
|
||||
const graphIDs = []
|
||||
// const graphIDs = []
|
||||
for (const node of graph) {
|
||||
const shouldKeep =
|
||||
varIDs.includes(node.variable.id) ||
|
||||
|
@ -139,20 +140,21 @@ export const findSubgraph = (
|
|||
|
||||
if (shouldKeep) {
|
||||
subgraph.add(node)
|
||||
graphIDs.push(node.variable.id)
|
||||
// graphIDs.push(node.variable.id)
|
||||
}
|
||||
}
|
||||
|
||||
const removeDupAncestors = (n: VariableNode) => {
|
||||
const {id} = n.variable
|
||||
return !graphIDs.includes(id)
|
||||
}
|
||||
// const removeDupAncestors = (n: VariableNode) => {
|
||||
// const {id} = n.variable
|
||||
// return !graphIDs.includes(id)
|
||||
// }
|
||||
|
||||
for (const node of subgraph) {
|
||||
node.parents = node.parents.filter(removeDupAncestors)
|
||||
node.children = node.children.filter(removeDupAncestors)
|
||||
// node.parents = node.parents.filter(removeDupAncestors)
|
||||
// node.children = node.children.filter(removeDupAncestors)
|
||||
node.parents = node.parents.filter(node => subgraph.has(node))
|
||||
node.children = node.children.filter(node => subgraph.has(node))
|
||||
}
|
||||
|
||||
return [...subgraph]
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue