Merge remote-tracking branch 'origin/master' into sgc/tsm1
commit
ca77c4f4b7
|
@ -32,6 +32,7 @@
|
|||
1. [19188](https://github.com/influxdata/influxdb/pull/19188): Dashboard cells correctly map results when multiple queries exist
|
||||
1. [19146](https://github.com/influxdata/influxdb/pull/19146): Dashboard cells and overlay use UTC as query time when toggling to UTC timezone
|
||||
1. [19222](https://github.com/influxdata/influxdb/pull/19222): Bucket names may not include quotation marks
|
||||
1. [19317](https://github.com/influxdata/influxdb/pull/19317): Add validation to Variable name creation for valid Flux identifiers.
|
||||
|
||||
### UI Improvements
|
||||
1. [19231](https://github.com/influxdata/influxdb/pull/19231): Alerts page filter inputs now have tab indices for keyboard navigation
|
||||
|
|
|
@ -241,8 +241,15 @@ func (h *OrgHandler) handleGetOrg(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// handleGetOrgs is the HTTP handler for the GET /api/v2/orgs route.
|
||||
func (h *OrgHandler) handleGetOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
var filter influxdb.OrganizationFilter
|
||||
qp := r.URL.Query()
|
||||
|
||||
opts, err := influxdb.DecodeFindOptions(r)
|
||||
if err != nil {
|
||||
h.API.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var filter influxdb.OrganizationFilter
|
||||
if name := qp.Get(Org); name != "" {
|
||||
filter.Name = &name
|
||||
}
|
||||
|
@ -264,7 +271,7 @@ func (h *OrgHandler) handleGetOrgs(w http.ResponseWriter, r *http.Request) {
|
|||
filter.UserID = id
|
||||
}
|
||||
|
||||
orgs, _, err := h.OrgSVC.FindOrganizations(r.Context(), filter)
|
||||
orgs, _, err := h.OrgSVC.FindOrganizations(r.Context(), filter, *opts)
|
||||
if err != nil {
|
||||
h.API.Err(w, r, err)
|
||||
return
|
||||
|
|
|
@ -2433,6 +2433,9 @@ paths:
|
|||
summary: Get all dashboards
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TraceSpan"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Descending"
|
||||
- in: query
|
||||
name: owner
|
||||
description: The owner ID.
|
||||
|
@ -3940,6 +3943,9 @@ paths:
|
|||
summary: List all organizations
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TraceSpan"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Descending"
|
||||
- in: query
|
||||
name: org
|
||||
schema:
|
||||
|
@ -4338,6 +4344,36 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"/orgs/{orgID}/owners/{userID}":
|
||||
delete:
|
||||
operationId: DeleteOrgsIDOwnersID
|
||||
tags:
|
||||
- Users
|
||||
- Organizations
|
||||
summary: Remove an owner from an organization
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TraceSpan"
|
||||
- in: path
|
||||
name: userID
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the owner to remove.
|
||||
- in: path
|
||||
name: orgID
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The organization ID.
|
||||
responses:
|
||||
"204":
|
||||
description: Owner removed
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/stacks:
|
||||
get:
|
||||
operationId: ListStacks
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/httprouter"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
|
@ -483,6 +484,7 @@ func (s *VariableService) FindVariables(ctx context.Context, filter influxdb.Var
|
|||
|
||||
// CreateVariable creates a new variable and assigns it an influxdb.ID
|
||||
func (s *VariableService) CreateVariable(ctx context.Context, m *influxdb.Variable) error {
|
||||
m.Name = strings.TrimSpace(m.Name)
|
||||
if err := m.Valid(); err != nil {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
platform "github.com/influxdata/influxdb/v2"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"github.com/influxdata/influxdb/v2/mock"
|
||||
platformtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
itesting "github.com/influxdata/influxdb/v2/testing"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
|
@ -58,7 +58,7 @@ func TestVariableService_handleGetVariables(t *testing.T) {
|
|||
FindVariablesF: func(ctx context.Context, filter platform.VariableFilter, opts ...platform.FindOptions) ([]*platform.Variable, error) {
|
||||
return []*platform.Variable{
|
||||
{
|
||||
ID: platformtesting.MustIDBase16("6162207574726f71"),
|
||||
ID: itesting.MustIDBase16("6162207574726f71"),
|
||||
OrganizationID: platform.ID(1),
|
||||
Name: "variable-a",
|
||||
Selected: []string{"b"},
|
||||
|
@ -72,7 +72,7 @@ func TestVariableService_handleGetVariables(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
ID: platformtesting.MustIDBase16("61726920617a696f"),
|
||||
ID: itesting.MustIDBase16("61726920617a696f"),
|
||||
OrganizationID: platform.ID(1),
|
||||
Name: "variable-b",
|
||||
Selected: []string{"c"},
|
||||
|
@ -92,7 +92,7 @@ func TestVariableService_handleGetVariables(t *testing.T) {
|
|||
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
|
||||
labels := []*platform.Label{
|
||||
{
|
||||
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
|
||||
ID: itesting.MustIDBase16("fc3dc670a4be9b9a"),
|
||||
Name: "label",
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
|
@ -211,8 +211,8 @@ func TestVariableService_handleGetVariables(t *testing.T) {
|
|||
FindVariablesF: func(ctx context.Context, filter platform.VariableFilter, opts ...platform.FindOptions) ([]*platform.Variable, error) {
|
||||
return []*platform.Variable{
|
||||
{
|
||||
ID: platformtesting.MustIDBase16("6162207574726f71"),
|
||||
OrganizationID: platformtesting.MustIDBase16("0000000000000001"),
|
||||
ID: itesting.MustIDBase16("6162207574726f71"),
|
||||
OrganizationID: itesting.MustIDBase16("0000000000000001"),
|
||||
Name: "variable-a",
|
||||
Selected: []string{"b"},
|
||||
Arguments: &platform.VariableArguments{
|
||||
|
@ -231,7 +231,7 @@ func TestVariableService_handleGetVariables(t *testing.T) {
|
|||
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
|
||||
labels := []*platform.Label{
|
||||
{
|
||||
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
|
||||
ID: itesting.MustIDBase16("fc3dc670a4be9b9a"),
|
||||
Name: "label",
|
||||
Properties: map[string]string{
|
||||
"color": "fff000",
|
||||
|
@ -362,7 +362,7 @@ func TestVariableService_handleGetVariable(t *testing.T) {
|
|||
&mock.VariableService{
|
||||
FindVariableByIDF: func(ctx context.Context, id platform.ID) (*platform.Variable, error) {
|
||||
return &platform.Variable{
|
||||
ID: platformtesting.MustIDBase16("75650d0a636f6d70"),
|
||||
ID: itesting.MustIDBase16("75650d0a636f6d70"),
|
||||
OrganizationID: platform.ID(1),
|
||||
Name: "variable-a",
|
||||
Selected: []string{"b"},
|
||||
|
@ -489,7 +489,7 @@ func TestVariableService_handlePostVariable(t *testing.T) {
|
|||
fields: fields{
|
||||
&mock.VariableService{
|
||||
CreateVariableF: func(ctx context.Context, m *platform.Variable) error {
|
||||
m.ID = platformtesting.MustIDBase16("75650d0a636f6d70")
|
||||
m.ID = itesting.MustIDBase16("75650d0a636f6d70")
|
||||
m.OrganizationID = platform.ID(1)
|
||||
m.UpdatedAt = faketime
|
||||
m.CreatedAt = faketime
|
||||
|
@ -529,7 +529,7 @@ func TestVariableService_handlePostVariable(t *testing.T) {
|
|||
fields: fields{
|
||||
&mock.VariableService{
|
||||
CreateVariableF: func(ctx context.Context, m *platform.Variable) error {
|
||||
m.ID = platformtesting.MustIDBase16("0")
|
||||
m.ID = itesting.MustIDBase16("0")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
@ -548,7 +548,7 @@ func TestVariableService_handlePostVariable(t *testing.T) {
|
|||
fields: fields{
|
||||
&mock.VariableService{
|
||||
CreateVariableF: func(ctx context.Context, m *platform.Variable) error {
|
||||
m.ID = platformtesting.MustIDBase16("0")
|
||||
m.ID = itesting.MustIDBase16("0")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
@ -621,7 +621,7 @@ func TestVariableService_handlePatchVariable(t *testing.T) {
|
|||
&mock.VariableService{
|
||||
UpdateVariableF: func(ctx context.Context, id platform.ID, u *platform.VariableUpdate) (*platform.Variable, error) {
|
||||
return &platform.Variable{
|
||||
ID: platformtesting.MustIDBase16("75650d0a636f6d70"),
|
||||
ID: itesting.MustIDBase16("75650d0a636f6d70"),
|
||||
OrganizationID: platform.ID(2),
|
||||
Name: "new-name",
|
||||
Arguments: &platform.VariableArguments{
|
||||
|
@ -888,7 +888,7 @@ func TestService_handlePostVariableLabel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func initVariableService(f platformtesting.VariableFields, t *testing.T) (platform.VariableService, string, func()) {
|
||||
func initVariableService(f itesting.VariableFields, t *testing.T) (platform.VariableService, string, func()) {
|
||||
svc := newInMemKVSVC(t)
|
||||
svc.IDGenerator = f.IDGenerator
|
||||
svc.TimeGenerator = f.TimeGenerator
|
||||
|
@ -915,5 +915,5 @@ func initVariableService(f platformtesting.VariableFields, t *testing.T) (platfo
|
|||
}
|
||||
|
||||
func TestVariableService(t *testing.T) {
|
||||
platformtesting.VariableService(initVariableService, t)
|
||||
itesting.VariableService(initVariableService, t, itesting.WithHTTPValidation())
|
||||
}
|
||||
|
|
|
@ -224,10 +224,6 @@ func decodeOrgDashboardIndexKey(indexKey []byte) (orgID influxdb.ID, dashID infl
|
|||
}
|
||||
|
||||
func (s *Service) findDashboards(ctx context.Context, tx Tx, filter influxdb.DashboardFilter, opts ...influxdb.FindOptions) ([]*influxdb.Dashboard, error) {
|
||||
if filter.OrganizationID != nil {
|
||||
return s.findOrganizationDashboards(ctx, tx, *filter.OrganizationID)
|
||||
}
|
||||
|
||||
var offset, limit, count int
|
||||
var descending bool
|
||||
if len(opts) > 0 {
|
||||
|
@ -236,6 +232,28 @@ func (s *Service) findDashboards(ctx context.Context, tx Tx, filter influxdb.Das
|
|||
descending = opts[0].Descending
|
||||
}
|
||||
|
||||
if filter.OrganizationID != nil {
|
||||
orgDashboards, err := s.findOrganizationDashboards(ctx, tx, *filter.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, &influxdb.Error{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if offset > 0 && offset < len(orgDashboards) {
|
||||
orgDashboards = orgDashboards[offset:]
|
||||
}
|
||||
if limit > 0 && limit < len(orgDashboards) {
|
||||
orgDashboards = orgDashboards[:limit]
|
||||
}
|
||||
if descending {
|
||||
for i, j := 0, len(orgDashboards)-1; i < j; i, j = i+1, j-1 {
|
||||
orgDashboards[i], orgDashboards[j] = orgDashboards[j], orgDashboards[i]
|
||||
}
|
||||
}
|
||||
|
||||
return orgDashboards, nil
|
||||
}
|
||||
|
||||
ds := []*influxdb.Dashboard{}
|
||||
filterFn := filterDashboardsFn(filter)
|
||||
err := s.forEachDashboard(ctx, tx, descending, func(d *influxdb.Dashboard) bool {
|
||||
|
|
44
kv/org.go
44
kv/org.go
|
@ -222,6 +222,14 @@ func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.Organiz
|
|||
return []*influxdb.Organization{o}, 1, nil
|
||||
}
|
||||
|
||||
var offset, limit, count int
|
||||
var descending bool
|
||||
if len(opt) > 0 {
|
||||
offset = opt[0].Offset
|
||||
limit = opt[0].Limit
|
||||
descending = opt[0].Descending
|
||||
}
|
||||
|
||||
os := []*influxdb.Organization{}
|
||||
|
||||
if filter.UserID != nil {
|
||||
|
@ -229,7 +237,7 @@ func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.Organiz
|
|||
urms, _, err := s.FindUserResourceMappings(ctx, influxdb.UserResourceMappingFilter{
|
||||
UserID: *filter.UserID,
|
||||
ResourceType: influxdb.OrgsResourceType,
|
||||
}, opt...)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
@ -239,7 +247,18 @@ func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.Organiz
|
|||
o, err := s.FindOrganizationByID(ctx, urm.ResourceID)
|
||||
if err == nil {
|
||||
// if there is an error then this is a crufty urm and we should just move on
|
||||
os = append(os, o)
|
||||
if count >= offset {
|
||||
os = append(os, o)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if limit > 0 && len(os) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
if descending {
|
||||
for i, j := 0, len(os)-1; i < j; i, j = i+1, j-1 {
|
||||
os[i], os[j] = os[j], os[i]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,9 +267,15 @@ func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.Organiz
|
|||
|
||||
filterFn := filterOrganizationsFn(filter)
|
||||
err := s.kv.View(ctx, func(tx Tx) error {
|
||||
return forEachOrganization(ctx, tx, func(o *influxdb.Organization) bool {
|
||||
if filterFn(o) {
|
||||
os = append(os, o)
|
||||
return forEachOrganization(ctx, tx, descending, func(o *influxdb.Organization) bool {
|
||||
if filterFn(o) { // TODO: Currently filterFn is useless here as we have finished all filtering jobs before. Keep it for future changes.
|
||||
if count >= offset {
|
||||
os = append(os, o)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if limit > 0 && len(os) >= limit {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
@ -398,13 +423,18 @@ func organizationIndexKey(n string) []byte {
|
|||
}
|
||||
|
||||
// forEachOrganization will iterate through all organizations while fn returns true.
|
||||
func forEachOrganization(ctx context.Context, tx Tx, fn func(*influxdb.Organization) bool) error {
|
||||
func forEachOrganization(ctx context.Context, tx Tx, descending bool, fn func(*influxdb.Organization) bool) error {
|
||||
b, err := tx.Bucket(organizationBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cur, err := b.ForwardCursor(nil)
|
||||
direction := CursorAscending
|
||||
if descending {
|
||||
direction = CursorDescending
|
||||
}
|
||||
|
||||
cur, err := b.ForwardCursor(nil, WithCursorDirection(direction))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -207,7 +207,6 @@ func (s *Service) CreateVariable(ctx context.Context, v *influxdb.Variable) erro
|
|||
}
|
||||
}
|
||||
|
||||
v.Name = strings.TrimSpace(v.Name) // TODO: move to service layer
|
||||
v.ID = s.IDGenerator.ID()
|
||||
now := s.Now()
|
||||
v.CreatedAt = now
|
||||
|
|
|
@ -141,8 +141,15 @@ func (h *OrgHandler) handleGetOrg(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// handleGetOrgs is the HTTP handler for the GET /api/v2/orgs route.
|
||||
func (h *OrgHandler) handleGetOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
var filter influxdb.OrganizationFilter
|
||||
qp := r.URL.Query()
|
||||
|
||||
opts, err := influxdb.DecodeFindOptions(r)
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var filter influxdb.OrganizationFilter
|
||||
if name := qp.Get("org"); name != "" {
|
||||
filter.Name = &name
|
||||
}
|
||||
|
@ -161,7 +168,7 @@ func (h *OrgHandler) handleGetOrgs(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
orgs, _, err := h.orgSvc.FindOrganizations(r.Context(), filter)
|
||||
orgs, _, err := h.orgSvc.FindOrganizations(r.Context(), filter, *opts)
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
|
|
|
@ -709,6 +709,44 @@ func FindDashboards(
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find all dashboards by offset and limit and org 1",
|
||||
fields: DashboardFields{
|
||||
Dashboards: []*platform.Dashboard{
|
||||
{
|
||||
ID: MustIDBase16(dashOneID),
|
||||
OrganizationID: 1,
|
||||
Name: "abc",
|
||||
},
|
||||
{
|
||||
ID: MustIDBase16(dashTwoID),
|
||||
OrganizationID: 1,
|
||||
Name: "xyz",
|
||||
},
|
||||
{
|
||||
ID: MustIDBase16(dashThreeID),
|
||||
OrganizationID: 1,
|
||||
Name: "321",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
findOptions: platform.FindOptions{
|
||||
Limit: 1,
|
||||
Offset: 1,
|
||||
},
|
||||
organizationID: idPtr(1),
|
||||
},
|
||||
wants: wants{
|
||||
dashboards: []*platform.Dashboard{
|
||||
{
|
||||
ID: MustIDBase16(dashTwoID),
|
||||
OrganizationID: 1,
|
||||
Name: "xyz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find all dashboards sorted by created at",
|
||||
fields: DashboardFields{
|
||||
|
|
|
@ -414,8 +414,9 @@ func FindOrganizations(
|
|||
t *testing.T,
|
||||
) {
|
||||
type args struct {
|
||||
ID influxdb.ID
|
||||
name string
|
||||
ID influxdb.ID
|
||||
name string
|
||||
findOptions influxdb.FindOptions
|
||||
}
|
||||
|
||||
type wants struct {
|
||||
|
@ -459,6 +460,42 @@ func FindOrganizations(
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find all organizations by offset and limit",
|
||||
fields: OrganizationFields{
|
||||
OrgBucketIDs: mock.NewIncrementingIDGenerator(idOne),
|
||||
Organizations: []*influxdb.Organization{
|
||||
{
|
||||
// ID(1)
|
||||
Name: "abc",
|
||||
},
|
||||
{
|
||||
// ID(2)
|
||||
Name: "xyz",
|
||||
Description: "desc xyz",
|
||||
},
|
||||
{
|
||||
// ID(3)
|
||||
Name: "ijk",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
findOptions: influxdb.FindOptions{
|
||||
Offset: 1,
|
||||
Limit: 1,
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
organizations: []*influxdb.Organization{
|
||||
{
|
||||
ID: idTwo,
|
||||
Name: "xyz",
|
||||
Description: "desc xyz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find organization by id",
|
||||
fields: OrganizationFields{
|
||||
|
@ -583,7 +620,7 @@ func FindOrganizations(
|
|||
filter.Name = &tt.args.name
|
||||
}
|
||||
|
||||
organizations, _, err := s.FindOrganizations(ctx, filter)
|
||||
organizations, _, err := s.FindOrganizations(ctx, filter, tt.args.findOptions)
|
||||
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
|
||||
|
||||
if diff := cmp.Diff(organizations, tt.wants.organizations, organizationCmpOptions...); diff != "" {
|
||||
|
|
1045
testing/variable.go
1045
testing/variable.go
File diff suppressed because it is too large
Load Diff
|
@ -4,6 +4,8 @@ $notebook-header-height: 46px;
|
|||
|
||||
$notebook-panel--gutter: $cf-marg-d;
|
||||
$notebook-panel--bg: mix($g1-raven, $g2-kevlar, 50%);
|
||||
$notebook-panel--node-gap: $cf-marg-d + $cf-marg-b;
|
||||
$notebook-panel--node-dot: 20px;
|
||||
|
||||
$notebook-divider-height: ($cf-marg-a * 2) + $cf-border;
|
||||
$notebook-divider-color: $g2-kevlar;
|
||||
|
|
|
@ -2,54 +2,56 @@
|
|||
@import '~src/notebooks/NotebookVariables.scss';
|
||||
|
||||
.notebook-divider {
|
||||
height: 12px;
|
||||
margin: $cf-marg-a $notebook-panel--gutter;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: $notebook-divider-color;
|
||||
height: $cf-border;
|
||||
transform: translateY(-50%);
|
||||
z-index: 0;
|
||||
}
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: $notebook-panel--node-gap;
|
||||
height: $notebook-panel--node-gap;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
z-index: 2;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 25vh;
|
||||
}
|
||||
}
|
||||
|
||||
.notebook-divider:hover:after,
|
||||
.notebook-divider__popped:after {
|
||||
background-color: $c-star;
|
||||
.notebook-panel:hover .notebook-divider,
|
||||
.notebook-panel:last-child .notebook-divider,
|
||||
.notebook-divider__popped {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notebook-panel:last-child .notebook-divider {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.notebook-panel:last-child .notebook-divider:after {
|
||||
content: '';
|
||||
width: $cf-border;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 5px;
|
||||
height: 150%;
|
||||
transform: translate(-50%, -100%);
|
||||
@include gradient-v($g5-pepper, $c-amethyst);
|
||||
}
|
||||
|
||||
.notebook-panel__hidden.notebook-panel:last-child .notebook-divider:after {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.notebook-divider--button {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.notebook-divider:hover &,
|
||||
.notebook-divider__popped &,
|
||||
.notebook-divider__popped:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.insert-cell-menu {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Libraries
|
||||
import React, {FC, useRef, useEffect, useContext} from 'react'
|
||||
import React, {FC, useRef, useContext} from 'react'
|
||||
|
||||
// Components
|
||||
import {
|
||||
|
@ -29,45 +29,8 @@ const InsertCellButton: FC<Props> = ({id}) => {
|
|||
const dividerRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const popoverVisible = useRef<boolean>(false)
|
||||
const buttonPositioningEnabled = useRef<boolean>(false)
|
||||
const index = notebook.data.indexOf(id)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMouseMove = (e: MouseEvent): void => {
|
||||
if (!dividerRef.current || !buttonRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
popoverVisible.current === false &&
|
||||
buttonPositioningEnabled.current === true
|
||||
) {
|
||||
const {pageX} = e
|
||||
const {left, width} = dividerRef.current.getBoundingClientRect()
|
||||
|
||||
const minLeft = 0
|
||||
const maxLeft = width
|
||||
|
||||
const buttonLeft = Math.min(Math.max(pageX - left, minLeft), maxLeft)
|
||||
buttonRef.current.setAttribute('style', `left: ${buttonLeft}px`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
buttonPositioningEnabled.current = true
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
buttonPositioningEnabled.current = false
|
||||
}
|
||||
|
||||
const handlePopoverShow = () => {
|
||||
popoverVisible.current = true
|
||||
dividerRef.current &&
|
||||
|
@ -81,12 +44,7 @@ const InsertCellButton: FC<Props> = ({id}) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="notebook-divider"
|
||||
ref={dividerRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="notebook-divider" ref={dividerRef}>
|
||||
<SquareButton
|
||||
icon={IconFont.Plus}
|
||||
ref={buttonRef}
|
||||
|
|
|
@ -23,6 +23,7 @@ import InsertCellButton from 'src/notebooks/components/panel/InsertCellButton'
|
|||
import PanelVisibilityToggle from 'src/notebooks/components/panel/PanelVisibilityToggle'
|
||||
import MovePanelButton from 'src/notebooks/components/panel/MovePanelButton'
|
||||
import NotebookPanelTitle from 'src/notebooks/components/panel/NotebookPanelTitle'
|
||||
import {FeatureFlag} from 'src/shared/utils/featureFlag'
|
||||
|
||||
// Types
|
||||
import {PipeContextProps} from 'src/notebooks'
|
||||
|
@ -66,6 +67,9 @@ const NotebookPanelHeader: FC<HeaderProps> = ({id, controls}) => {
|
|||
|
||||
return (
|
||||
<div className="notebook-panel--header">
|
||||
<div className="notebook-panel--node-wrapper">
|
||||
<div className="notebook-panel--node" />
|
||||
</div>
|
||||
<FlexBox
|
||||
className="notebook-panel--header-left"
|
||||
alignItems={AlignItems.Center}
|
||||
|
@ -81,16 +85,18 @@ const NotebookPanelHeader: FC<HeaderProps> = ({id, controls}) => {
|
|||
justifyContent={JustifyContent.FlexEnd}
|
||||
>
|
||||
{controls}
|
||||
<MovePanelButton
|
||||
direction="up"
|
||||
onClick={moveUp}
|
||||
active={canBeMovedUp}
|
||||
/>
|
||||
<MovePanelButton
|
||||
direction="down"
|
||||
onClick={moveDown}
|
||||
active={canBeMovedDown}
|
||||
/>
|
||||
<FeatureFlag name="notebook-move-cells">
|
||||
<MovePanelButton
|
||||
direction="up"
|
||||
onClick={moveUp}
|
||||
active={canBeMovedUp}
|
||||
/>
|
||||
<MovePanelButton
|
||||
direction="down"
|
||||
onClick={moveDown}
|
||||
active={canBeMovedDown}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
<PanelVisibilityToggle id={id} />
|
||||
<RemovePanelButton onRemove={remove} />
|
||||
</FlexBox>
|
||||
|
@ -139,15 +145,13 @@ const NotebookPanel: FC<Props> = ({id, children, controls}) => {
|
|||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ClickOutside onClickOutside={handleClickOutside}>
|
||||
<div className={panelClassName} onClick={handleClick} ref={panelRef}>
|
||||
<NotebookPanelHeader id={id} controls={controls} />
|
||||
<div className="notebook-panel--body">{children}</div>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
{!notebook.readOnly && <InsertCellButton id={id} />}
|
||||
</>
|
||||
<ClickOutside onClickOutside={handleClickOutside}>
|
||||
<div className={panelClassName} onClick={handleClick} ref={panelRef}>
|
||||
<NotebookPanelHeader id={id} controls={controls} />
|
||||
<div className="notebook-panel--body">{children}</div>
|
||||
{!notebook.readOnly && <InsertCellButton id={id} />}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ register({
|
|||
button: 'Flux Script',
|
||||
initial: {
|
||||
panelVisibility: 'visible',
|
||||
panelHeight: 200,
|
||||
panelHeight: 330,
|
||||
activeQuery: 0,
|
||||
queries: [
|
||||
{
|
||||
|
|
|
@ -30,11 +30,30 @@
|
|||
}
|
||||
|
||||
.notebook-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-radius: $cf-radius;
|
||||
margin: 0 $notebook-panel--gutter;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: $cf-border;
|
||||
background-color: $g5-pepper;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: ($notebook-panel--node-gap / 2) - ($cf-border / 2);
|
||||
}
|
||||
|
||||
&:first-child::after {
|
||||
top: ($notebook-header-height - $notebook-panel--node-dot) / 2;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 25vh;
|
||||
}
|
||||
}
|
||||
|
||||
.notebook-panel--header,
|
||||
|
@ -46,7 +65,7 @@
|
|||
border-radius: $cf-radius $cf-radius 0 0;
|
||||
height: $notebook-header-height;
|
||||
flex: 0 0 $notebook-header-height;
|
||||
padding: 0 $cf-marg-b;
|
||||
padding-right: $cf-marg-b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
@ -62,6 +81,45 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.notebook-panel--node-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
width: $notebook-panel--node-gap;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.notebook-panel--node {
|
||||
width: $notebook-panel--node-dot;
|
||||
height: $notebook-panel--node-dot;
|
||||
border-radius: 50%;
|
||||
background-color: $g1-raven;
|
||||
border: $cf-border solid $g5-pepper;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border-radius: 50%;
|
||||
background-color: $c-laser;
|
||||
transform: translate(-50%, -50%) scale(1.5, 1.5);
|
||||
width: $cf-marg-b;
|
||||
height: $cf-marg-b;
|
||||
box-shadow: 0 0 8px 2px $c-void, 0 0 4px 2px $c-pool, 0 0 1px 1px$c-laser;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.notebook-panel__focus &:after {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.notebook-panel--title,
|
||||
.notebook-panel--data-source {
|
||||
font-size: 14px;
|
||||
|
@ -121,7 +179,7 @@
|
|||
.notebook-panel--body {
|
||||
border-radius: 0 0 $cf-radius $cf-radius;
|
||||
padding: $cf-marg-b;
|
||||
padding-left: $cf-marg-d;
|
||||
padding-left: $notebook-panel--node-gap;
|
||||
padding-top: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export const OSS_FLAGS = {
|
|||
notebooks: false,
|
||||
telegrafEditor: false,
|
||||
streamEvents: false,
|
||||
'notebook-move-cells': false,
|
||||
'notebook-panel--spotify': false,
|
||||
'notebook-panel--test-flux': false,
|
||||
disableDefaultTableSort: false,
|
||||
|
@ -36,6 +37,7 @@ export const CLOUD_FLAGS = {
|
|||
notebooks: false,
|
||||
telegrafEditor: false,
|
||||
streamEvents: false,
|
||||
'notebook-move-cells': false,
|
||||
'notebook-panel--spotify': false,
|
||||
'notebook-panel--test-flux': false,
|
||||
disableDefaultTableSort: false,
|
||||
|
|
|
@ -37,5 +37,11 @@ export const validateVariableName = (
|
|||
}
|
||||
}
|
||||
|
||||
if (!varName[0].match(/[A-Z]|[_]/i)) {
|
||||
return {
|
||||
error: `Variable name must begin with a letter or underscore`,
|
||||
}
|
||||
}
|
||||
|
||||
return {error: null}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// ErrVariableNotFound is the error msg for a missing variable.
|
||||
|
@ -117,6 +118,11 @@ func (m *Variable) Valid() error {
|
|||
return fmt.Errorf("missing variable name")
|
||||
}
|
||||
|
||||
// variable name must start with a letter to be a valid identifier in Flux
|
||||
if !regexp.MustCompile(`^[a-zA-Z_].*`).MatchString(m.Name) {
|
||||
return fmt.Errorf("variable name must start with a letter")
|
||||
}
|
||||
|
||||
validTypes := map[string]bool{
|
||||
"constant": true,
|
||||
"map": true,
|
||||
|
|
Loading…
Reference in New Issue