Merge remote-tracking branch 'origin/master' into sgc/tsm1

pull/19446/head
Stuart Carnie 2020-08-21 13:06:45 -07:00
commit ca77c4f4b7
No known key found for this signature in database
GPG Key ID: 848D9C9718D78B4F
21 changed files with 895 additions and 627 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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())
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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{

View File

@ -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 != "" {

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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 {

View File

@ -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}

View File

@ -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>
)
}

View File

@ -10,7 +10,7 @@ register({
button: 'Flux Script',
initial: {
panelVisibility: 'visible',
panelHeight: 200,
panelHeight: 330,
activeQuery: 0,
queries: [
{

View File

@ -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;
}

View File

@ -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,

View File

@ -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}
}

View File

@ -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,