Merge remote-tracking branch 'origin/master' into sgc/tsm1
commit
b3734695c5
|
@ -1,3 +1,9 @@
|
|||
## v2.0.0-beta.17 [unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
1. [19331](https://github.com/influxdata/influxdb/pull/19331): Add description to auth influx command outputs.
|
||||
|
||||
## v2.0.0-beta.16 [2020-08-07]
|
||||
|
||||
### Breaking
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
type token struct {
|
||||
ID platform.ID `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Token string `json:"token"`
|
||||
Status string `json:"status"`
|
||||
UserName string `json:"userName"`
|
||||
|
@ -43,8 +44,9 @@ var authCRUDFlags struct {
|
|||
}
|
||||
|
||||
var authCreateFlags struct {
|
||||
user string
|
||||
org organization
|
||||
user string
|
||||
description string
|
||||
org organization
|
||||
|
||||
writeUserPermission bool
|
||||
readUserPermission bool
|
||||
|
@ -90,6 +92,7 @@ func authCreateCmd(f *globalFlags) *cobra.Command {
|
|||
f.registerFlags(cmd)
|
||||
authCreateFlags.org.register(cmd, false)
|
||||
|
||||
cmd.Flags().StringVarP(&authCreateFlags.description, "description", "d", "", "Token description")
|
||||
cmd.Flags().StringVarP(&authCreateFlags.user, "user", "u", "", "The user name")
|
||||
registerPrintOptions(cmd, &authCRUDFlags.hideHeaders, &authCRUDFlags.json)
|
||||
|
||||
|
@ -250,6 +253,7 @@ func authorizationCreateF(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
authorization := &platform.Authorization{
|
||||
Description: authCreateFlags.description,
|
||||
Permissions: permissions,
|
||||
OrgID: orgID,
|
||||
}
|
||||
|
@ -288,6 +292,7 @@ func authorizationCreateF(cmd *cobra.Command, args []string) error {
|
|||
hideHeaders: authCRUDFlags.hideHeaders,
|
||||
token: token{
|
||||
ID: authorization.ID,
|
||||
Description: authorization.Description,
|
||||
Token: authorization.Token,
|
||||
Status: string(authorization.Status),
|
||||
UserName: user.Name,
|
||||
|
@ -381,6 +386,7 @@ func authorizationFindF(cmd *cobra.Command, args []string) error {
|
|||
|
||||
tokens = append(tokens, token{
|
||||
ID: a.ID,
|
||||
Description: a.Description,
|
||||
Token: a.Token,
|
||||
Status: string(a.Status),
|
||||
UserName: user.Name,
|
||||
|
@ -453,6 +459,7 @@ func authorizationDeleteF(cmd *cobra.Command, args []string) error {
|
|||
hideHeaders: authCRUDFlags.hideHeaders,
|
||||
token: token{
|
||||
ID: a.ID,
|
||||
Description: a.Description,
|
||||
Token: a.Token,
|
||||
Status: string(a.Status),
|
||||
UserName: user.Name,
|
||||
|
@ -520,6 +527,7 @@ func authorizationActiveF(cmd *cobra.Command, args []string) error {
|
|||
hideHeaders: authCRUDFlags.hideHeaders,
|
||||
token: token{
|
||||
ID: a.ID,
|
||||
Description: a.Description,
|
||||
Token: a.Token,
|
||||
Status: string(a.Status),
|
||||
UserName: user.Name,
|
||||
|
@ -587,6 +595,7 @@ func authorizationInactiveF(cmd *cobra.Command, args []string) error {
|
|||
hideHeaders: authCRUDFlags.hideHeaders,
|
||||
token: token{
|
||||
ID: a.ID,
|
||||
Description: a.Description,
|
||||
Token: a.Token,
|
||||
Status: string(a.Status),
|
||||
UserName: user.Name,
|
||||
|
@ -620,6 +629,7 @@ func writeTokens(w io.Writer, printOpts tokenPrintOpt) error {
|
|||
|
||||
headers := []string{
|
||||
"ID",
|
||||
"Description",
|
||||
"Token",
|
||||
"User Name",
|
||||
"User ID",
|
||||
|
@ -637,6 +647,7 @@ func writeTokens(w io.Writer, printOpts tokenPrintOpt) error {
|
|||
for _, t := range printOpts.tokens {
|
||||
m := map[string]interface{}{
|
||||
"ID": t.ID.String(),
|
||||
"Description": t.Description,
|
||||
"Token": t.Token,
|
||||
"User Name": t.UserName,
|
||||
"User ID": t.UserID.String(),
|
||||
|
|
14
flags.yml
14
flags.yml
|
@ -120,6 +120,14 @@
|
|||
contact: Query Team
|
||||
lifetime: temporary
|
||||
|
||||
- name: Mosaic Graph Type
|
||||
description: Enables the creation of a mosaic graph in Dashboards
|
||||
key: mosaicGraphType
|
||||
default: false
|
||||
contact: Monitoring Team
|
||||
expose: true
|
||||
lifetime: temporary
|
||||
|
||||
- name: Notebooks
|
||||
description: Determine if the notebook feature's route and navbar icon are visible to the user
|
||||
key: notebooks
|
||||
|
@ -133,3 +141,9 @@
|
|||
key: pushDownGroupAggregateMinMax
|
||||
default: false
|
||||
contact: Query Team
|
||||
|
||||
- name: Org Only Member list
|
||||
description: Enforce only org members have access to view members of org related resorces
|
||||
key: orgOnlyMemberList
|
||||
default: false
|
||||
contact: Compute Team
|
||||
|
|
|
@ -212,6 +212,20 @@ func MergedFiltersRule() BoolFlag {
|
|||
return mergeFiltersRule
|
||||
}
|
||||
|
||||
var mosaicGraphType = MakeBoolFlag(
|
||||
"Mosaic Graph Type",
|
||||
"mosaicGraphType",
|
||||
"Monitoring Team",
|
||||
false,
|
||||
Temporary,
|
||||
true,
|
||||
)
|
||||
|
||||
// MosaicGraphType - Enables the creation of a mosaic graph in Dashboards
|
||||
func MosaicGraphType() BoolFlag {
|
||||
return mosaicGraphType
|
||||
}
|
||||
|
||||
var notebooks = MakeBoolFlag(
|
||||
"Notebooks",
|
||||
"notebooks",
|
||||
|
@ -240,6 +254,20 @@ func PushDownGroupAggregateMinMax() BoolFlag {
|
|||
return pushDownGroupAggregateMinMax
|
||||
}
|
||||
|
||||
var orgOnlyMemberList = MakeBoolFlag(
|
||||
"Org Only Member list",
|
||||
"orgOnlyMemberList",
|
||||
"Compute Team",
|
||||
false,
|
||||
Temporary,
|
||||
false,
|
||||
)
|
||||
|
||||
// OrgOnlyMemberList - Enforce only org members have access to view members of org related resorces
|
||||
func OrgOnlyMemberList() BoolFlag {
|
||||
return orgOnlyMemberList
|
||||
}
|
||||
|
||||
var all = []Flag{
|
||||
appMetrics,
|
||||
backendExample,
|
||||
|
@ -256,8 +284,10 @@ var all = []Flag{
|
|||
simpleTaskOptionsExtraction,
|
||||
useUserPermission,
|
||||
mergeFiltersRule,
|
||||
mosaicGraphType,
|
||||
notebooks,
|
||||
pushDownGroupAggregateMinMax,
|
||||
orgOnlyMemberList,
|
||||
}
|
||||
|
||||
var byKey = map[string]Flag{
|
||||
|
@ -276,6 +306,8 @@ var byKey = map[string]Flag{
|
|||
"simpleTaskOptionsExtraction": simpleTaskOptionsExtraction,
|
||||
"useUserPermission": useUserPermission,
|
||||
"mergeFiltersRule": mergeFiltersRule,
|
||||
"mosaicGraphType": mosaicGraphType,
|
||||
"notebooks": notebooks,
|
||||
"pushDownGroupAggregateMinMax": pushDownGroupAggregateMinMax,
|
||||
"orgOnlyMemberList": orgOnlyMemberList,
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/authorizer"
|
||||
"github.com/influxdata/influxdb/v2/kit/feature"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
)
|
||||
|
||||
|
@ -21,13 +22,23 @@ func NewAuthedURMService(orgSvc influxdb.OrganizationService, s influxdb.UserRes
|
|||
}
|
||||
|
||||
func (s *AuthedURMService) FindUserResourceMappings(ctx context.Context, filter influxdb.UserResourceMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.UserResourceMapping, int, error) {
|
||||
orgID := kithttp.OrgIDFromContext(ctx) // resource's orgID
|
||||
|
||||
if feature.OrgOnlyMemberList().Enabled(ctx) {
|
||||
// Check if user making request has read access to organization prior to listing URMs.
|
||||
if orgID != nil {
|
||||
if _, _, err := authorizer.AuthorizeReadResource(ctx, influxdb.OrgsResourceType, *orgID); err != nil {
|
||||
return nil, 0, ErrNotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
urms, _, err := s.s.FindUserResourceMappings(ctx, filter, opt...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
authedUrms := urms[:0]
|
||||
orgID := kithttp.OrgIDFromContext(ctx)
|
||||
for _, urm := range urms {
|
||||
if orgID != nil {
|
||||
if _, _, err := authorizer.AuthorizeRead(ctx, urm.ResourceType, urm.ResourceID, *orgID); err != nil {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
influxdbcontext "github.com/influxdata/influxdb/v2/context"
|
||||
"github.com/influxdata/influxdb/v2/kit/feature"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"github.com/influxdata/influxdb/v2/mock"
|
||||
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
|
||||
|
@ -36,7 +37,7 @@ func TestURMService_FindUserResourceMappings(t *testing.T) {
|
|||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "authorized to see all users by org auth",
|
||||
name: "authorized to see all users",
|
||||
fields: fields{
|
||||
UserResourceMappingService: &mock.UserResourceMappingService{
|
||||
FindMappingsFn: func(ctx context.Context, filter influxdb.UserResourceMappingFilter) ([]*influxdb.UserResourceMapping, int, error) {
|
||||
|
@ -59,6 +60,13 @@ func TestURMService_FindUserResourceMappings(t *testing.T) {
|
|||
},
|
||||
args: args{
|
||||
permissions: []influxdb.Permission{
|
||||
{
|
||||
Action: "read",
|
||||
Resource: influxdb.Resource{
|
||||
Type: influxdb.OrgsResourceType,
|
||||
ID: influxdbtesting.IDPtr(10),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: "read",
|
||||
Resource: influxdb.Resource{
|
||||
|
@ -100,6 +108,43 @@ func TestURMService_FindUserResourceMappings(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authorized to see all users by org auth",
|
||||
fields: fields{
|
||||
UserResourceMappingService: &mock.UserResourceMappingService{
|
||||
FindMappingsFn: func(ctx context.Context, filter influxdb.UserResourceMappingFilter) ([]*influxdb.UserResourceMapping, int, error) {
|
||||
return []*influxdb.UserResourceMapping{
|
||||
{
|
||||
ResourceID: 1,
|
||||
ResourceType: influxdb.BucketsResourceType,
|
||||
},
|
||||
{
|
||||
ResourceID: 2,
|
||||
ResourceType: influxdb.BucketsResourceType,
|
||||
},
|
||||
{
|
||||
ResourceID: 3,
|
||||
ResourceType: influxdb.BucketsResourceType,
|
||||
},
|
||||
}, 3, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
permissions: []influxdb.Permission{
|
||||
{
|
||||
Action: "read",
|
||||
Resource: influxdb.Resource{
|
||||
Type: influxdb.BucketsResourceType,
|
||||
OrgID: influxdbtesting.IDPtr(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
err: ErrNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -108,6 +153,13 @@ func TestURMService_FindUserResourceMappings(t *testing.T) {
|
|||
orgID := influxdbtesting.IDPtr(10)
|
||||
ctx := context.WithValue(context.Background(), kithttp.CtxOrgKey, *orgID)
|
||||
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, tt.args.permissions))
|
||||
ctx, _ = feature.Annotate(ctx, feature.DefaultFlagger(), feature.MakeBoolFlag("Org Only Member list",
|
||||
"orgOnlyMemberList",
|
||||
"Compute Team",
|
||||
true,
|
||||
feature.Temporary,
|
||||
false,
|
||||
))
|
||||
|
||||
urms, _, err := s.FindUserResourceMappings(ctx, influxdb.UserResourceMappingFilter{})
|
||||
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
|
||||
|
|
|
@ -157,68 +157,87 @@ describe('Community Templates', () => {
|
|||
})
|
||||
|
||||
it('Can click on template resources', () => {
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//buckets
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('Bucket')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left') // force a click on the far left of the target, in case the text is aligned left and short
|
||||
cy.url().should('include', 'load-data/buckets')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//telegraf
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('Telegraf')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'load-data/telegrafs')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//check
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('Check')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'alerting/checks')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//label
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('Label')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'settings/labels')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//Dashboard
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('Dashboard')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'dashboards')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//Notification Endpoint
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('NotificationEndpoint')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'alerting')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//Notification Rule
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('NotificationRule')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'alerting')
|
||||
cy.go('back')
|
||||
|
||||
cy.getByTestID('template-resource-link').click()
|
||||
//Variable
|
||||
cy.getByTestID('template-resource-link')
|
||||
cy.get('.community-templates--resources-table')
|
||||
.contains('Variable')
|
||||
.click()
|
||||
.siblings('td')
|
||||
.click('left')
|
||||
cy.url().should('include', 'settings/variables')
|
||||
cy.go('back')
|
||||
})
|
||||
|
||||
it('Click on source takes you to github', () => {
|
||||
cy.getByTestID('template-source-link').should(
|
||||
'contain',
|
||||
'https://github.com/influxdata/community-templates/blob/master/docker/docker.yml'
|
||||
)
|
||||
it('takes you to github when you click on the Community Templates link', () => {
|
||||
cy.getByTestID('template-source-link').within(() => {
|
||||
cy.contains('Community Templates').should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'https://github.com/influxdata/community-templates/blob/master/docker/docker.yml'
|
||||
)
|
||||
})
|
||||
//TODO: add the link from CLI
|
||||
})
|
||||
|
||||
|
|
|
@ -696,11 +696,17 @@ describe('DataExplorer', () => {
|
|||
|
||||
// cycle through all the visualizations of the data
|
||||
VIS_TYPES.forEach(({type}) => {
|
||||
cy.getByTestID('view-type--dropdown').click()
|
||||
cy.getByTestID(`view-type--${type}`).click()
|
||||
cy.getByTestID(`vis-graphic--${type}`).should('exist')
|
||||
if (type.includes('single-stat')) {
|
||||
cy.getByTestID('single-stat--text').should('contain', `${numLines}`)
|
||||
if (type != 'mosaic') {
|
||||
//mosaic graph is behind feature flag
|
||||
cy.getByTestID('view-type--dropdown').click()
|
||||
cy.getByTestID(`view-type--${type}`).click()
|
||||
cy.getByTestID(`vis-graphic--${type}`).should('exist')
|
||||
if (type.includes('single-stat')) {
|
||||
cy.getByTestID('single-stat--text').should(
|
||||
'contain',
|
||||
`${numLines}`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
createEndpoint,
|
||||
createDashWithCell,
|
||||
createDashWithViewAndVar,
|
||||
createRule,
|
||||
} from './support/commands'
|
||||
|
||||
declare global {
|
||||
|
@ -67,6 +68,7 @@ declare global {
|
|||
createToken: typeof createToken
|
||||
writeData: typeof writeData
|
||||
createEndpoint: typeof createEndpoint
|
||||
createRule: typeof createRule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -359,6 +359,50 @@ export const createTelegraf = (
|
|||
})
|
||||
}
|
||||
|
||||
export const createRule = (
|
||||
orgID: string,
|
||||
endpointID: string,
|
||||
name = ''
|
||||
): Cypress.Chainable<Cypress.Response> => {
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/v2/notificationRules',
|
||||
body: genRule({endpointID, orgID, name}),
|
||||
})
|
||||
}
|
||||
|
||||
type RuleArgs = {
|
||||
endpointID: string
|
||||
orgID: string
|
||||
type?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
const genRule = ({
|
||||
endpointID,
|
||||
orgID,
|
||||
type = 'slack',
|
||||
name = 'r1',
|
||||
}: RuleArgs) => ({
|
||||
type,
|
||||
every: '20m',
|
||||
offset: '1m',
|
||||
url: '',
|
||||
orgID,
|
||||
name,
|
||||
activeStatus: 'active',
|
||||
status: 'active',
|
||||
endpointID,
|
||||
tagRules: [],
|
||||
labels: [],
|
||||
statusRules: [
|
||||
{currentLevel: 'CRIT', period: '1h', count: 1, previousLevel: 'INFO'},
|
||||
],
|
||||
description: '',
|
||||
messageTemplate: 'im a message',
|
||||
channel: '',
|
||||
})
|
||||
|
||||
/*
|
||||
[{action: 'write', resource: {type: 'views'}},
|
||||
{action: 'write', resource: {type: 'documents'}},
|
||||
|
@ -478,6 +522,8 @@ export const createEndpoint = (
|
|||
/* eslint-disable */
|
||||
// notification endpoints
|
||||
Cypress.Commands.add('createEndpoint', createEndpoint)
|
||||
// notification rules
|
||||
Cypress.Commands.add('createRule', createRule)
|
||||
|
||||
// assertions
|
||||
Cypress.Commands.add('fluxEqual', fluxEqual)
|
||||
|
|
|
@ -50,13 +50,13 @@ import {
|
|||
addBucketLabelFailed,
|
||||
removeBucketLabelFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
import {LIMIT} from 'src/resources/constants'
|
||||
import {BUCKET_LIMIT} from 'src/resources/constants'
|
||||
|
||||
type Action = BucketAction | NotifyAction
|
||||
|
||||
export const fetchAllBuckets = async (orgID: string) => {
|
||||
const resp = await api.getBuckets({
|
||||
query: {orgID, limit: LIMIT},
|
||||
query: {orgID, limit: BUCKET_LIMIT},
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
|
|
|
@ -32,6 +32,7 @@ import {setLabelOnResource} from 'src/labels/actions/creators'
|
|||
import {draftRuleToPostRule} from 'src/notifications/rules/utils'
|
||||
import {getOrg} from 'src/organizations/selectors'
|
||||
import {getAll, getStatus} from 'src/resources/selectors'
|
||||
import {incrementCloneName} from 'src/utils/naming'
|
||||
|
||||
// Types
|
||||
import {
|
||||
|
@ -45,7 +46,9 @@ import {
|
|||
RuleEntities,
|
||||
ResourceType,
|
||||
} from 'src/types'
|
||||
import {incrementCloneName} from 'src/utils/naming'
|
||||
|
||||
// Constants
|
||||
import {RULE_LIMIT} from 'src/resources/constants'
|
||||
|
||||
export const getNotificationRules = () => async (
|
||||
dispatch: Dispatch<
|
||||
|
@ -64,7 +67,9 @@ export const getNotificationRules = () => async (
|
|||
|
||||
const {id: orgID} = getOrg(state)
|
||||
|
||||
const resp = await api.getNotificationRules({query: {orgID}})
|
||||
const resp = await api.getNotificationRules({
|
||||
query: {orgID, limit: RULE_LIMIT},
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(resp.data.message)
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
// TODO: temporary fix until we implement pagination https://github.com/influxdata/influxdb/pull/17336
|
||||
// Different resources *might* have different limits
|
||||
export const LIMIT = 100
|
||||
export const BUCKET_LIMIT = 100
|
||||
export const RULE_LIMIT = 100
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
// Libraries
|
||||
import React, {FunctionComponent} from 'react'
|
||||
import {Config, Table} from '@influxdata/giraffe'
|
||||
|
||||
// Components
|
||||
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
useVisXDomainSettings,
|
||||
useVisYDomainSettings,
|
||||
} from 'src/shared/utils/useVisDomainSettings'
|
||||
import {getFormatter, defaultXColumn, mosaicYcolumn} from 'src/shared/utils/vis'
|
||||
|
||||
// Constants
|
||||
import {VIS_THEME, VIS_THEME_LIGHT} from 'src/shared/constants'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
import {INVALID_DATA_COPY} from 'src/shared/copy/cell'
|
||||
|
||||
// Types
|
||||
import {MosaicViewProperties, TimeZone, TimeRange, Theme} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
children: (config: Config) => JSX.Element
|
||||
fluxGroupKeyUnion?: string[]
|
||||
timeRange: TimeRange | null
|
||||
table: Table
|
||||
timeZone: TimeZone
|
||||
viewProperties: MosaicViewProperties
|
||||
theme?: Theme
|
||||
}
|
||||
|
||||
const MosaicPlot: FunctionComponent<Props> = ({
|
||||
children,
|
||||
timeRange,
|
||||
timeZone,
|
||||
table,
|
||||
viewProperties: {
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
fillColumns: storedFill,
|
||||
colors,
|
||||
xDomain: storedXDomain,
|
||||
yDomain: storedYDomain,
|
||||
xColumn: storedXColumn,
|
||||
ySeriesColumns: storedYColumn,
|
||||
timeFormat,
|
||||
},
|
||||
theme,
|
||||
}) => {
|
||||
const fillColumns = storedFill || []
|
||||
const xColumn = storedXColumn || defaultXColumn(table)
|
||||
let yColumn
|
||||
if (storedYColumn) {
|
||||
yColumn = storedYColumn[0]
|
||||
} else {
|
||||
yColumn = mosaicYcolumn(table)
|
||||
}
|
||||
const columnKeys = table.columnKeys
|
||||
|
||||
const [xDomain, onSetXDomain, onResetXDomain] = useVisXDomainSettings(
|
||||
storedXDomain,
|
||||
table.getColumn(xColumn, 'number'),
|
||||
timeRange
|
||||
)
|
||||
|
||||
const [yDomain, onSetYDomain, onResetYDomain] = useVisYDomainSettings(
|
||||
storedYDomain,
|
||||
table.getColumn(yColumn, 'string')
|
||||
)
|
||||
|
||||
const isValidView =
|
||||
xColumn &&
|
||||
columnKeys.includes(xColumn) &&
|
||||
yColumn &&
|
||||
columnKeys.includes(yColumn) &&
|
||||
fillColumns.length !== 0 &&
|
||||
fillColumns.every(col => columnKeys.includes(col))
|
||||
|
||||
if (!isValidView) {
|
||||
return <EmptyGraphMessage message={INVALID_DATA_COPY} />
|
||||
}
|
||||
|
||||
const colorHexes =
|
||||
colors && colors.length ? colors : DEFAULT_LINE_COLORS.map(c => c.hex)
|
||||
|
||||
const xFormatter = getFormatter(table.getColumnType(xColumn), {
|
||||
timeZone,
|
||||
timeFormat,
|
||||
})
|
||||
|
||||
const yFormatter = getFormatter(table.getColumnType(yColumn), {
|
||||
timeZone,
|
||||
timeFormat,
|
||||
})
|
||||
|
||||
const currentTheme = theme === 'light' ? VIS_THEME_LIGHT : VIS_THEME
|
||||
|
||||
const config: Config = {
|
||||
...currentTheme,
|
||||
table,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
xDomain,
|
||||
onSetXDomain,
|
||||
onResetXDomain,
|
||||
yDomain,
|
||||
onSetYDomain,
|
||||
onResetYDomain,
|
||||
valueFormatters: {
|
||||
[xColumn]: xFormatter,
|
||||
[yColumn]: yFormatter,
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
type: 'mosaic',
|
||||
x: xColumn,
|
||||
y: yColumn,
|
||||
colors: colorHexes,
|
||||
fill: fillColumns,
|
||||
},
|
||||
],
|
||||
}
|
||||
return children(config)
|
||||
}
|
||||
export default MosaicPlot
|
|
@ -8,6 +8,7 @@ import SingleStat from 'src/shared/components/SingleStat'
|
|||
import TableGraphs from 'src/shared/components/tables/TableGraphs'
|
||||
import HistogramPlot from 'src/shared/components/HistogramPlot'
|
||||
import HeatmapPlot from 'src/shared/components/HeatmapPlot'
|
||||
import MosaicPlot from 'src/shared/components/MosaicPlot'
|
||||
import FluxTablesTransform from 'src/shared/components/FluxTablesTransform'
|
||||
import XYPlot from 'src/shared/components/XYPlot'
|
||||
import ScatterPlot from 'src/shared/components/ScatterPlot'
|
||||
|
@ -175,6 +176,19 @@ const ViewSwitcher: FunctionComponent<Props> = ({
|
|||
</HeatmapPlot>
|
||||
)
|
||||
|
||||
case 'mosaic':
|
||||
return (
|
||||
<MosaicPlot
|
||||
timeRange={timeRange}
|
||||
table={table}
|
||||
timeZone={timeZone}
|
||||
viewProperties={properties}
|
||||
theme={theme}
|
||||
>
|
||||
{config => <Plot config={config} />}
|
||||
</MosaicPlot>
|
||||
)
|
||||
|
||||
case 'scatter':
|
||||
return (
|
||||
<ScatterPlot
|
||||
|
|
|
@ -19,7 +19,7 @@ import {TimeRange} from 'src/types'
|
|||
passed to the plot.
|
||||
*/
|
||||
export const getValidRange = (
|
||||
data: NumericColumnData = [],
|
||||
data: string[] | NumericColumnData = [],
|
||||
timeRange: TimeRange | null
|
||||
) => {
|
||||
const range = extent(data as number[])
|
||||
|
@ -60,7 +60,7 @@ const isValidStoredDomainValue = (value): boolean => {
|
|||
}
|
||||
|
||||
export const getRemainingRange = (
|
||||
data: NumericColumnData = [],
|
||||
data: string[] | NumericColumnData = [],
|
||||
timeRange: TimeRange | null,
|
||||
storedDomain: number[]
|
||||
) => {
|
||||
|
@ -81,7 +81,7 @@ export const getRemainingRange = (
|
|||
|
||||
export const useVisYDomainSettings = (
|
||||
storedDomain: number[],
|
||||
data: NumericColumnData,
|
||||
data: NumericColumnData | string[],
|
||||
timeRange: TimeRange | null = null
|
||||
) => {
|
||||
const initialDomain = useMemo(() => {
|
||||
|
@ -99,6 +99,5 @@ export const useVisYDomainSettings = (
|
|||
|
||||
const [domain, setDomain] = useOneWayState(initialDomain)
|
||||
const resetDomain = () => setDomain(initialDomain)
|
||||
|
||||
return [domain, setDomain, resetDomain]
|
||||
}
|
||||
|
|
|
@ -210,6 +210,20 @@ export const getNumberColumns = (table: Table): string[] => {
|
|||
return numberColumnKeys
|
||||
}
|
||||
|
||||
export const getStringColumns = (table: Table): string[] => {
|
||||
const stringColumnKeys = table.columnKeys.filter(k => {
|
||||
if (k === 'result' || k === 'table') {
|
||||
return false
|
||||
}
|
||||
|
||||
const columnType = table.getColumnType(k)
|
||||
|
||||
return columnType === 'string'
|
||||
})
|
||||
|
||||
return stringColumnKeys
|
||||
}
|
||||
|
||||
export const getGroupableColumns = (table: Table): string[] => {
|
||||
const invalidGroupColumns = new Set(['_value', '_time', 'table'])
|
||||
const groupableColumns = table.columnKeys.filter(
|
||||
|
@ -235,6 +249,7 @@ export const getGroupableColumns = (table: Table): string[] => {
|
|||
A `null` result from this function indicates that no valid selection could be
|
||||
made.
|
||||
*/
|
||||
|
||||
export const defaultXColumn = (
|
||||
table: Table,
|
||||
preferredColumnKey?: string
|
||||
|
@ -282,6 +297,34 @@ export const defaultYColumn = (
|
|||
return null
|
||||
}
|
||||
|
||||
export const mosaicYcolumn = (
|
||||
table: Table,
|
||||
preferredColumnKey?: string
|
||||
): string | null => {
|
||||
const validColumnKeys = getStringColumns(table)
|
||||
if (validColumnKeys.includes(preferredColumnKey)) {
|
||||
return preferredColumnKey
|
||||
}
|
||||
|
||||
const invalidMosaicYColumns = new Set([
|
||||
'_value',
|
||||
'status',
|
||||
'_field',
|
||||
'_measurement',
|
||||
])
|
||||
const preferredValidColumnKeys = validColumnKeys.filter(
|
||||
name => !invalidMosaicYColumns.has(name)
|
||||
)
|
||||
if (preferredValidColumnKeys.length) {
|
||||
return preferredValidColumnKeys[0]
|
||||
}
|
||||
|
||||
if (validColumnKeys.length) {
|
||||
return validColumnKeys[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const isInDomain = (value: number, domain: number[]) =>
|
||||
value >= domain[0] && value <= domain[1]
|
||||
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {connect, ConnectedProps} from 'react-redux'
|
||||
|
||||
// Selectors
|
||||
import {getAll} from 'src/resources/selectors'
|
||||
|
||||
// Types
|
||||
import {
|
||||
AppState,
|
||||
Dashboard,
|
||||
Task,
|
||||
Label,
|
||||
Bucket,
|
||||
Telegraf,
|
||||
Variable,
|
||||
ResourceType,
|
||||
Check,
|
||||
NotificationEndpoint,
|
||||
NotificationRule,
|
||||
} from 'src/types'
|
||||
|
||||
interface ComponentProps {
|
||||
link: string
|
||||
metaName: string
|
||||
resourceID: string
|
||||
}
|
||||
|
||||
type ReduxProps = ConnectedProps<typeof connector>
|
||||
|
||||
type Props = ComponentProps & ReduxProps
|
||||
|
||||
const CommunityTemplateHumanReadableResourceUnconnected: FC<Props> = ({
|
||||
link,
|
||||
metaName,
|
||||
resourceID,
|
||||
allResources,
|
||||
}) => {
|
||||
const matchingResource = allResources.find(
|
||||
resource => resource.id === resourceID
|
||||
)
|
||||
|
||||
const humanName = matchingResource ? matchingResource.name : metaName
|
||||
|
||||
return (
|
||||
<Link to={link}>
|
||||
<code>{humanName}</code>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const labels = getAll<Label>(state, ResourceType.Labels)
|
||||
const cleanedLabels = labels.map(label => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
}))
|
||||
|
||||
const buckets = getAll<Bucket>(state, ResourceType.Buckets)
|
||||
const cleanedBuckets = buckets.map(bucket => ({
|
||||
id: bucket.id,
|
||||
name: bucket.name,
|
||||
}))
|
||||
|
||||
const telegrafs = getAll<Telegraf>(state, ResourceType.Telegrafs)
|
||||
const cleanedTelegrafs = telegrafs.map(telegraf => ({
|
||||
id: telegraf.id,
|
||||
name: telegraf.name,
|
||||
}))
|
||||
|
||||
const variables = getAll<Variable>(state, ResourceType.Variables)
|
||||
const cleanedVariables = variables.map(variable => ({
|
||||
id: variable.id,
|
||||
name: variable.name,
|
||||
}))
|
||||
|
||||
const dashboards = getAll<Dashboard>(state, ResourceType.Dashboards)
|
||||
const cleanedDashboards = dashboards.map(dashboard => ({
|
||||
id: dashboard.id,
|
||||
name: dashboard.name,
|
||||
}))
|
||||
|
||||
const tasks = getAll<Task>(state, ResourceType.Tasks)
|
||||
const cleanedTasks = tasks.map(task => ({
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
}))
|
||||
|
||||
const checks = getAll<Check>(state, ResourceType.Checks)
|
||||
const cleanedChecks = checks.map(check => ({
|
||||
id: check.id,
|
||||
name: check.name,
|
||||
}))
|
||||
|
||||
const notificationEndpoints = getAll<NotificationEndpoint>(
|
||||
state,
|
||||
ResourceType.NotificationEndpoints
|
||||
)
|
||||
const cleanedNotificationEndpoints = notificationEndpoints.map(
|
||||
notificationEndpoint => ({
|
||||
id: notificationEndpoint.id,
|
||||
name: notificationEndpoint.name,
|
||||
})
|
||||
)
|
||||
|
||||
const notificationRules = getAll<NotificationRule>(
|
||||
state,
|
||||
ResourceType.NotificationRules
|
||||
)
|
||||
const cleanedNotificationRules = notificationRules.map(notificationRule => {
|
||||
if (notificationRule.id && notificationRule.name) {
|
||||
return {
|
||||
id: notificationRule.id,
|
||||
name: notificationRule.name,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const allResources = [
|
||||
...cleanedLabels,
|
||||
...cleanedBuckets,
|
||||
...cleanedTelegrafs,
|
||||
...cleanedVariables,
|
||||
...cleanedDashboards,
|
||||
...cleanedTasks,
|
||||
...cleanedChecks,
|
||||
...cleanedNotificationEndpoints,
|
||||
...cleanedNotificationRules,
|
||||
]
|
||||
|
||||
return {
|
||||
allResources,
|
||||
}
|
||||
}
|
||||
|
||||
const connector = connect(mstp)
|
||||
|
||||
export const CommunityTemplateHumanReadableResource = connector(
|
||||
CommunityTemplateHumanReadableResourceUnconnected
|
||||
)
|
|
@ -1,6 +1,5 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {connect, ConnectedProps} from 'react-redux'
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
// Components
|
||||
import {
|
||||
|
@ -11,7 +10,12 @@ import {
|
|||
ConfirmationButton,
|
||||
IconFont,
|
||||
Table,
|
||||
LinkButton,
|
||||
VerticalAlignment,
|
||||
ButtonShape,
|
||||
Alignment,
|
||||
} from '@influxdata/clockface'
|
||||
import {CommunityTemplatesResourceSummary} from 'src/templates/components/CommunityTemplatesResourceSummary'
|
||||
|
||||
// Redux
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
|
@ -40,7 +44,7 @@ type ReduxProps = ConnectedProps<typeof connector>
|
|||
|
||||
type Props = OwnProps & ReduxProps
|
||||
|
||||
interface Resource {
|
||||
export interface Resource {
|
||||
apiVersion?: string
|
||||
resourceID?: string
|
||||
kind?: TemplateKind
|
||||
|
@ -61,137 +65,19 @@ class CommunityTemplatesInstalledListUnconnected extends PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
private renderStackResources(resources: Resource[]) {
|
||||
return resources.map(resource => {
|
||||
switch (resource.kind) {
|
||||
case 'Bucket': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link to={`/orgs/${this.props.orgID}/load-data/buckets`}>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'Check':
|
||||
case 'CheckDeadman':
|
||||
case 'CheckThreshold': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/alerting/checks/${resource.resourceID}/edit`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'Dashboard': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/dashboards/${resource.resourceID}`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'Label': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link to={`/orgs/${this.props.orgID}/settings/labels`}>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'NotificationEndpoint':
|
||||
case 'NotificationEndpointHTTP':
|
||||
case 'NotificationEndpointPagerDuty':
|
||||
case 'NotificationEndpointSlack': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/alerting/endpoints/${resource.resourceID}/edit`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'NotificationRule': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/alerting/rules/${resource.resourceID}/edit`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'Task': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/tasks/${resource.resourceID}/edit`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'Telegraf': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/load-data/telegrafs/${resource.resourceID}/view`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
case 'Variable': {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
<Link
|
||||
to={`/orgs/${this.props.orgID}/settings/variables/${resource.resourceID}/edit`}
|
||||
>
|
||||
{resource.kind} <code>{resource.templateMetaName}</code>
|
||||
</Link>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<React.Fragment key={resource.templateMetaName}>
|
||||
{resource.kind}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private renderStackSources(sources: string[]) {
|
||||
return sources.map(source => {
|
||||
if (source.includes('github')) {
|
||||
if (source.includes('github') && source.includes('influxdata')) {
|
||||
return (
|
||||
<a key={source} href={source}>
|
||||
{source}
|
||||
</a>
|
||||
<LinkButton
|
||||
key={source}
|
||||
text="Community Templates"
|
||||
icon={IconFont.GitHub}
|
||||
href={source}
|
||||
size={ComponentSize.Small}
|
||||
style={{display: 'inline-block'}}
|
||||
target="_blank"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -228,11 +114,21 @@ class CommunityTemplatesInstalledListUnconnected extends PureComponent<Props> {
|
|||
<Table striped={true} highlight={true}>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Template Name</Table.HeaderCell>
|
||||
<Table.HeaderCell>Resources Created</Table.HeaderCell>
|
||||
<Table.HeaderCell>Install Date</Table.HeaderCell>
|
||||
<Table.HeaderCell>Source</Table.HeaderCell>
|
||||
<Table.HeaderCell> </Table.HeaderCell>
|
||||
<Table.HeaderCell style={{width: '250px'}}>
|
||||
Template Name
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell style={{width: 'calc(100% - 700px)'}}>
|
||||
Installed Resources
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell style={{width: '180px'}}>
|
||||
Install Date
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell style={{width: '210px'}}>
|
||||
Source
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell style={{width: '60px'}}>
|
||||
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
|
@ -242,19 +138,39 @@ class CommunityTemplatesInstalledListUnconnected extends PureComponent<Props> {
|
|||
testID="installed-template-list"
|
||||
key={`stack-${stack.id}`}
|
||||
>
|
||||
<Table.Cell testID={`installed-template-${stack.name}`}>
|
||||
{stack.name}
|
||||
<Table.Cell
|
||||
testID={`installed-template-${stack.name}`}
|
||||
verticalAlignment={VerticalAlignment.Top}
|
||||
>
|
||||
<span className="community-templates--resources-table-item">
|
||||
{stack.name}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell testID="template-resource-link">
|
||||
{this.renderStackResources(stack.resources)}
|
||||
<Table.Cell
|
||||
testID="template-resource-link"
|
||||
verticalAlignment={VerticalAlignment.Top}
|
||||
>
|
||||
<CommunityTemplatesResourceSummary
|
||||
resources={stack.resources}
|
||||
stackID={stack.id}
|
||||
orgID={this.props.orgID}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{new Date(stack.createdAt).toDateString()}
|
||||
<Table.Cell verticalAlignment={VerticalAlignment.Top}>
|
||||
<span className="community-templates--resources-table-item">
|
||||
{new Date(stack.createdAt).toDateString()}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell testID="template-source-link">
|
||||
<Table.Cell
|
||||
testID="template-source-link"
|
||||
verticalAlignment={VerticalAlignment.Top}
|
||||
>
|
||||
{this.renderStackSources(stack.sources)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Table.Cell
|
||||
verticalAlignment={VerticalAlignment.Top}
|
||||
horizontalAlignment={Alignment.Right}
|
||||
>
|
||||
<ConfirmationButton
|
||||
confirmationButtonText="Delete"
|
||||
testID={`template-delete-button-${stack.name}`}
|
||||
|
@ -270,6 +186,7 @@ class CommunityTemplatesInstalledListUnconnected extends PureComponent<Props> {
|
|||
color={ComponentColor.Danger}
|
||||
size={ComponentSize.Small}
|
||||
status={ComponentStatus.Default}
|
||||
shape={ButtonShape.Square}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
// Libraries
|
||||
import React, {FC, useState} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components
|
||||
import {Icon, IconFont} from '@influxdata/clockface'
|
||||
import {CommunityTemplateHumanReadableResource} from 'src/templates/components/CommunityTemplateHumanReadableResource'
|
||||
|
||||
// Types
|
||||
import {Resource} from 'src/templates/components/CommunityTemplatesInstalledList'
|
||||
|
||||
interface Props {
|
||||
resources: Resource[]
|
||||
stackID: string
|
||||
orgID: string
|
||||
}
|
||||
|
||||
export const CommunityTemplatesResourceSummary: FC<Props> = ({
|
||||
resources,
|
||||
stackID,
|
||||
orgID,
|
||||
}) => {
|
||||
const [summaryState, setSummaryState] = useState<'expanded' | 'collapsed'>(
|
||||
'collapsed'
|
||||
)
|
||||
|
||||
const toggleText = `${resources.length} Resource${
|
||||
!!resources.length ? 's' : ''
|
||||
}`
|
||||
|
||||
const handleToggleTable = (): void => {
|
||||
if (summaryState === 'expanded') {
|
||||
setSummaryState('collapsed')
|
||||
} else {
|
||||
setSummaryState('expanded')
|
||||
}
|
||||
}
|
||||
|
||||
const caretClassName = classnames(
|
||||
'community-templates--resources-table-caret',
|
||||
{
|
||||
'community-templates--resources-table-caret__expanded':
|
||||
summaryState === 'expanded',
|
||||
}
|
||||
)
|
||||
|
||||
const tableRows = resources.map(resource => {
|
||||
switch (resource.kind) {
|
||||
case 'Bucket': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/load-data/buckets`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'Check':
|
||||
case 'CheckDeadman':
|
||||
case 'CheckThreshold': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/alerting/checks/${resource.resourceID}/edit`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'Dashboard': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/dashboards/${resource.resourceID}`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'Label': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/settings/labels`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'NotificationEndpoint':
|
||||
case 'NotificationEndpointHTTP':
|
||||
case 'NotificationEndpointPagerDuty':
|
||||
case 'NotificationEndpointSlack': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/alerting/endpoints/${resource.resourceID}/edit`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'NotificationRule': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/alerting/rules/${resource.resourceID}/edit`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'Task': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/tasks/${resource.resourceID}/edit`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'Telegraf': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/load-data/telegrafs/${resource.resourceID}/view`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
case 'Variable': {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
<td>
|
||||
<CommunityTemplateHumanReadableResource
|
||||
link={`/orgs/${orgID}/settings/variables/${resource.resourceID}/edit`}
|
||||
metaName={resource.templateMetaName}
|
||||
resourceID={resource.resourceID}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<tr key={`${resource.templateMetaName}-${stackID}-${resource.kind}`}>
|
||||
<td>{resource.kind}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="community-templates--resources-table-toggle"
|
||||
onClick={handleToggleTable}
|
||||
>
|
||||
<Icon className={caretClassName} glyph={IconFont.CaretRight} />
|
||||
<h6>{toggleText}</h6>
|
||||
</div>
|
||||
{summaryState === 'expanded' && (
|
||||
<table className="community-templates--resources-table">
|
||||
<tbody>{tableRows}</tbody>
|
||||
</table>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -3,10 +3,24 @@
|
|||
grid-column-gap: $ix-marg-b;
|
||||
grid-row-gap: $ix-marg-b;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 80px 150px;
|
||||
margin: $ix-marg-b 0;
|
||||
}
|
||||
|
||||
/* The two styles below are a bandaid fix until the clockface dependency can be safely updated */
|
||||
.cf-panel.community-templates-panel
|
||||
.cf-panel--symbol-header.cf-panel--symbol-header__sm {
|
||||
padding-left: 46px;
|
||||
padding-right: 46px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $cf-grid--breakpoint-sm) {
|
||||
.cf-panel.community-templates-panel
|
||||
.cf-panel--symbol-header.cf-panel--symbol-header__sm {
|
||||
padding-left: 70px;
|
||||
padding-right: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.community-templates-template-url {
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
|
@ -94,3 +108,38 @@
|
|||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.community-templates--resources-table {
|
||||
& > tbody > tr > td {
|
||||
padding-right: $cf-marg-b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.community-templates--resources-table-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.community-templates--resources-table-caret {
|
||||
margin-right: $cf-marg-b;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.community-templates--resources-table-caret__expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.community-templates--resources-table-toggle h6,
|
||||
.community-templates--resources-table-item {
|
||||
height: $cf-form-sm-height;
|
||||
line-height: $cf-form-sm-height;
|
||||
}
|
||||
|
|
|
@ -26,12 +26,15 @@ import {
|
|||
LinkTarget,
|
||||
Page,
|
||||
Panel,
|
||||
FlexDirection,
|
||||
IconFont,
|
||||
} from '@influxdata/clockface'
|
||||
import SettingsTabbedPage from 'src/settings/components/SettingsTabbedPage'
|
||||
import SettingsHeader from 'src/settings/components/SettingsHeader'
|
||||
|
||||
import {communityTemplatesImportPath} from 'src/templates/containers/TemplatesIndex'
|
||||
|
||||
import GetResources from 'src/resources/components/GetResources'
|
||||
import {getOrg} from 'src/organizations/selectors'
|
||||
|
||||
// Utils
|
||||
|
@ -44,7 +47,7 @@ import {reportError} from 'src/shared/utils/errors'
|
|||
|
||||
import {communityTemplateUnsupportedFormatError} from 'src/shared/copy/notifications'
|
||||
// Types
|
||||
import {AppState} from 'src/types'
|
||||
import {AppState, ResourceType} from 'src/types'
|
||||
|
||||
const communityTemplatesUrl =
|
||||
'https://github.com/influxdata/community-templates#templates'
|
||||
|
@ -94,7 +97,7 @@ class UnconnectedTemplatesIndex extends Component<Props> {
|
|||
<SettingsTabbedPage activeTab="templates" orgID={org.id}>
|
||||
{/* todo: maybe make this not a div */}
|
||||
<div className="community-templates-upload">
|
||||
<Panel className="community-templates-upload-panel">
|
||||
<Panel className="community-templates-panel">
|
||||
<Panel.SymbolHeader
|
||||
symbol={<Bullet text={1} size={ComponentSize.Medium} />}
|
||||
title={
|
||||
|
@ -107,10 +110,11 @@ class UnconnectedTemplatesIndex extends Component<Props> {
|
|||
<LinkButton
|
||||
color={ComponentColor.Primary}
|
||||
href={communityTemplatesUrl}
|
||||
size={ComponentSize.Small}
|
||||
size={ComponentSize.Large}
|
||||
target={LinkTarget.Blank}
|
||||
text="Browse Community Templates"
|
||||
testID="browse-template-button"
|
||||
icon={IconFont.GitHub}
|
||||
/>
|
||||
</Panel.SymbolHeader>
|
||||
</Panel>
|
||||
|
@ -122,28 +126,44 @@ class UnconnectedTemplatesIndex extends Component<Props> {
|
|||
Paste the Template's Github URL below
|
||||
</Heading>
|
||||
}
|
||||
size={ComponentSize.Medium}
|
||||
size={ComponentSize.Small}
|
||||
/>
|
||||
<Panel.Body size={ComponentSize.Large}>
|
||||
<div>
|
||||
<Input
|
||||
className="community-templates-template-url"
|
||||
onChange={this.handleTemplateChange}
|
||||
placeholder="Enter the URL of an InfluxDB Template..."
|
||||
style={{width: '80%'}}
|
||||
value={this.state.templateUrl}
|
||||
testID="lookup-template-input"
|
||||
/>
|
||||
<Button
|
||||
onClick={this.startTemplateInstall}
|
||||
size={ComponentSize.Small}
|
||||
text="Lookup Template"
|
||||
testID="lookup-template-button"
|
||||
/>
|
||||
</div>
|
||||
<Panel.Body
|
||||
size={ComponentSize.Large}
|
||||
direction={FlexDirection.Row}
|
||||
>
|
||||
<Input
|
||||
className="community-templates-template-url"
|
||||
onChange={this.handleTemplateChange}
|
||||
placeholder="Enter the URL of an InfluxDB Template..."
|
||||
style={{flex: '1 0 0'}}
|
||||
value={this.state.templateUrl}
|
||||
testID="lookup-template-input"
|
||||
size={ComponentSize.Large}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.startTemplateInstall}
|
||||
size={ComponentSize.Large}
|
||||
text="Lookup Template"
|
||||
testID="lookup-template-button"
|
||||
/>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
<CommunityTemplatesInstalledList orgID={org.id} />
|
||||
<GetResources
|
||||
resources={[
|
||||
ResourceType.Buckets,
|
||||
ResourceType.Checks,
|
||||
ResourceType.Dashboards,
|
||||
ResourceType.Labels,
|
||||
ResourceType.NotificationEndpoints,
|
||||
ResourceType.NotificationRules,
|
||||
ResourceType.Tasks,
|
||||
ResourceType.Telegrafs,
|
||||
ResourceType.Variables,
|
||||
]}
|
||||
>
|
||||
<CommunityTemplatesInstalledList orgID={org.id} />
|
||||
</GetResources>
|
||||
</div>
|
||||
</SettingsTabbedPage>
|
||||
</Page>
|
||||
|
|
|
@ -77,6 +77,7 @@ export type Action =
|
|||
| SetTimeFormatAction
|
||||
| SetXColumnAction
|
||||
| SetYColumnAction
|
||||
| SetYSeriesColumnsAction
|
||||
| SetBinSizeAction
|
||||
| SetColorHexesAction
|
||||
| SetFillColumnsAction
|
||||
|
@ -529,6 +530,18 @@ export const setYColumn = (yColumn: string): SetYColumnAction => ({
|
|||
payload: {yColumn},
|
||||
})
|
||||
|
||||
interface SetYSeriesColumnsAction {
|
||||
type: 'SET_Y_SERIES_COLUMNS'
|
||||
payload: {ySeriesColumns: string[]}
|
||||
}
|
||||
|
||||
export const setYSeriesColumns = (
|
||||
ySeriesColumns: string[]
|
||||
): SetYSeriesColumnsAction => ({
|
||||
type: 'SET_Y_SERIES_COLUMNS',
|
||||
payload: {ySeriesColumns},
|
||||
})
|
||||
|
||||
interface SetShadeBelowAction {
|
||||
type: 'SET_SHADE_BELOW'
|
||||
payload: {shadeBelow}
|
||||
|
|
|
@ -136,7 +136,6 @@ const mstp = (state: AppState) => {
|
|||
const yColumn = getYColumnSelection(state)
|
||||
const fillColumns = getFillColumnsSelection(state)
|
||||
const symbolColumns = getSymbolColumnsSelection(state)
|
||||
|
||||
const timeZone = getTimeZone(state)
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
// Libraries
|
||||
import React, {SFC} from 'react'
|
||||
import {connect, ConnectedProps} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import {Form, Grid, Input} from '@influxdata/clockface'
|
||||
import TimeFormat from 'src/timeMachine/components/view_options/TimeFormat'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
setFillColumns,
|
||||
setYAxisLabel,
|
||||
setXAxisLabel,
|
||||
setColorHexes,
|
||||
setYDomain,
|
||||
setXColumn,
|
||||
setYSeriesColumns,
|
||||
setTimeFormat,
|
||||
} from 'src/timeMachine/actions'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
getGroupableColumns,
|
||||
getFillColumnsSelection,
|
||||
getXColumnSelection,
|
||||
getYColumnSelection,
|
||||
getNumericColumns,
|
||||
getStringColumns,
|
||||
getActiveTimeMachine,
|
||||
} from 'src/timeMachine/selectors'
|
||||
|
||||
// Constants
|
||||
import {GIRAFFE_COLOR_SCHEMES} from 'src/shared/constants'
|
||||
|
||||
// Types
|
||||
import {AppState, NewView, MosaicViewProperties} from 'src/types'
|
||||
import HexColorSchemeDropdown from 'src/shared/components/HexColorSchemeDropdown'
|
||||
import ColumnSelector from 'src/shared/components/ColumnSelector'
|
||||
|
||||
interface OwnProps {
|
||||
xColumn: string
|
||||
yColumn: string
|
||||
fillColumns: string
|
||||
xDomain: number[]
|
||||
yDomain: number[]
|
||||
xAxisLabel: string
|
||||
yAxisLabel: string
|
||||
colors: string[]
|
||||
showNoteWhenEmpty: boolean
|
||||
}
|
||||
|
||||
type ReduxProps = ConnectedProps<typeof connector>
|
||||
type Props = OwnProps & ReduxProps
|
||||
|
||||
const MosaicOptions: SFC<Props> = props => {
|
||||
const {
|
||||
fillColumns,
|
||||
yAxisLabel,
|
||||
xAxisLabel,
|
||||
onSetFillColumns,
|
||||
colors,
|
||||
onSetColors,
|
||||
onSetYAxisLabel,
|
||||
onSetXAxisLabel,
|
||||
xColumn,
|
||||
yColumn,
|
||||
stringColumns,
|
||||
numericColumns,
|
||||
onSetXColumn,
|
||||
onSetYSeriesColumns,
|
||||
onSetTimeFormat,
|
||||
timeFormat,
|
||||
} = props
|
||||
|
||||
const handleFillColumnSelect = (column: string): void => {
|
||||
const fillColumn = [column]
|
||||
onSetFillColumns(fillColumn)
|
||||
}
|
||||
|
||||
const onSelectYSeriesColumns = (colName: string) => {
|
||||
onSetYSeriesColumns([colName])
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid.Column>
|
||||
<h4 className="view-options--header">Customize Mosaic Plot</h4>
|
||||
<h5 className="view-options--header">Data</h5>
|
||||
<ColumnSelector
|
||||
selectedColumn={fillColumns[0]}
|
||||
onSelectColumn={handleFillColumnSelect}
|
||||
availableColumns={stringColumns}
|
||||
axisName="fill"
|
||||
/>
|
||||
<ColumnSelector
|
||||
selectedColumn={xColumn}
|
||||
onSelectColumn={onSetXColumn}
|
||||
availableColumns={numericColumns}
|
||||
axisName="x"
|
||||
/>
|
||||
<ColumnSelector
|
||||
selectedColumn={yColumn}
|
||||
onSelectColumn={onSelectYSeriesColumns}
|
||||
availableColumns={stringColumns}
|
||||
axisName="y"
|
||||
/>
|
||||
<Form.Element label="Time Format">
|
||||
<TimeFormat
|
||||
timeFormat={timeFormat}
|
||||
onTimeFormatChange={onSetTimeFormat}
|
||||
/>
|
||||
</Form.Element>
|
||||
<h5 className="view-options--header">Options</h5>
|
||||
<Form.Element label="Color Scheme">
|
||||
<HexColorSchemeDropdown
|
||||
colorSchemes={GIRAFFE_COLOR_SCHEMES}
|
||||
selectedColorScheme={colors}
|
||||
onSelectColorScheme={onSetColors}
|
||||
/>
|
||||
</Form.Element>
|
||||
<h5 className="view-options--header">X Axis</h5>
|
||||
<Form.Element label="X Axis Label">
|
||||
<Input
|
||||
value={xAxisLabel}
|
||||
onChange={e => onSetXAxisLabel(e.target.value)}
|
||||
/>
|
||||
</Form.Element>
|
||||
<h5 className="view-options--header">Y Axis</h5>
|
||||
<Form.Element label="Y Axis Label">
|
||||
<Input
|
||||
value={yAxisLabel}
|
||||
onChange={e => onSetYAxisLabel(e.target.value)}
|
||||
/>
|
||||
</Form.Element>{' '}
|
||||
</Grid.Column>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const availableGroupColumns = getGroupableColumns(state)
|
||||
const fillColumns = getFillColumnsSelection(state)
|
||||
const xColumn = getXColumnSelection(state)
|
||||
const yColumn = getYColumnSelection(state)
|
||||
const stringColumns = getStringColumns(state)
|
||||
const numericColumns = getNumericColumns(state)
|
||||
const view = getActiveTimeMachine(state).view as NewView<MosaicViewProperties>
|
||||
const {timeFormat} = view.properties
|
||||
|
||||
return {
|
||||
availableGroupColumns,
|
||||
fillColumns,
|
||||
xColumn,
|
||||
yColumn,
|
||||
stringColumns,
|
||||
numericColumns,
|
||||
timeFormat,
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onSetFillColumns: setFillColumns,
|
||||
onSetColors: setColorHexes,
|
||||
onSetYAxisLabel: setYAxisLabel,
|
||||
onSetXAxisLabel: setXAxisLabel,
|
||||
onSetYDomain: setYDomain,
|
||||
onSetXColumn: setXColumn,
|
||||
onSetYSeriesColumns: setYSeriesColumns,
|
||||
onSetTimeFormat: setTimeFormat,
|
||||
}
|
||||
|
||||
const connector = connect(mstp, mdtp)
|
||||
export default connector(MosaicOptions)
|
|
@ -9,6 +9,7 @@ import TableOptions from 'src/timeMachine/components/view_options/TableOptions'
|
|||
import HistogramOptions from 'src/timeMachine/components/view_options/HistogramOptions'
|
||||
import HeatmapOptions from 'src/timeMachine/components/view_options/HeatmapOptions'
|
||||
import ScatterOptions from 'src/timeMachine/components/view_options/ScatterOptions'
|
||||
import MosaicOptions from 'src/timeMachine/components/view_options/MosaicOptions'
|
||||
|
||||
// Types
|
||||
import {View, NewView} from 'src/types'
|
||||
|
@ -43,6 +44,8 @@ class OptionsSwitcher extends PureComponent<Props> {
|
|||
return <HeatmapOptions {...view.properties} />
|
||||
case 'scatter':
|
||||
return <ScatterOptions {...view.properties} />
|
||||
case 'mosaic':
|
||||
return <MosaicOptions {...view.properties} />
|
||||
default:
|
||||
return <div />
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {Dropdown, DropdownMenuTheme} from '@influxdata/clockface'
|
|||
|
||||
// Utils
|
||||
import {getActiveTimeMachine} from 'src/timeMachine/selectors'
|
||||
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
|
||||
|
||||
// Constants
|
||||
import {VIS_GRAPHICS} from 'src/timeMachine/constants/visGraphics'
|
||||
|
@ -53,18 +54,25 @@ class ViewTypeDropdown extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
private get dropdownItems(): JSX.Element[] {
|
||||
return VIS_GRAPHICS.map(g => (
|
||||
<Dropdown.Item
|
||||
key={`view-type--${g.type}`}
|
||||
id={`${g.type}`}
|
||||
testID={`view-type--${g.type}`}
|
||||
value={g.type}
|
||||
onClick={this.handleChange}
|
||||
selected={`${g.type}` === this.selectedView}
|
||||
>
|
||||
{this.getVewTypeGraphic(g.type)}
|
||||
</Dropdown.Item>
|
||||
))
|
||||
return VIS_GRAPHICS.filter(g => {
|
||||
if (g.type === 'mosaic' && !isFlagEnabled('mosaicGraphType')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}).map(g => {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={`view-type--${g.type}`}
|
||||
id={`${g.type}`}
|
||||
testID={`view-type--${g.type}`}
|
||||
value={g.type}
|
||||
onClick={this.handleChange}
|
||||
selected={`${g.type}` === this.selectedView}
|
||||
>
|
||||
{this.getVewTypeGraphic(g.type)}
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get dropdownStatus(): ComponentStatus {
|
||||
|
|
|
@ -18,6 +18,10 @@ export const VIS_TYPES: VisType[] = [
|
|||
type: 'heatmap',
|
||||
name: 'Heatmap',
|
||||
},
|
||||
{
|
||||
type: 'mosaic',
|
||||
name: 'Mosaic',
|
||||
},
|
||||
{
|
||||
type: 'histogram',
|
||||
name: 'Histogram',
|
||||
|
|
|
@ -19,12 +19,12 @@ const GRAPHIC_SVGS = {
|
|||
<g id="r">
|
||||
<polygon
|
||||
className="vis-graphic--fill vis-graphic--fill-b"
|
||||
points="127.5,127.5 127.5,112.5 112.5,112.5 112.5,127.5 97.5,127.5 97.5,142.5 112.5,142.5 127.5,142.5
|
||||
points="127.5,127.5 127.5,112.5 112.5,112.5 112.5,127.5 97.5,127.5 97.5,142.5 112.5,142.5 127.5,142.5
|
||||
142.5,142.5 142.5,127.5 "
|
||||
/>
|
||||
<polygon
|
||||
className="vis-graphic--fill vis-graphic--fill-b"
|
||||
points="67.5,127.5 52.5,127.5 52.5,112.5 37.5,112.5 37.5,97.5 22.5,97.5 22.5,112.5 22.5,127.5 7.5,127.5
|
||||
points="67.5,127.5 52.5,127.5 52.5,112.5 37.5,112.5 37.5,97.5 22.5,97.5 22.5,112.5 22.5,127.5 7.5,127.5
|
||||
7.5,142.5 22.5,142.5 37.5,142.5 52.5,142.5 67.5,142.5 82.5,142.5 82.5,127.5 82.5,112.5 67.5,112.5 "
|
||||
/>
|
||||
</g>
|
||||
|
@ -47,18 +47,18 @@ const GRAPHIC_SVGS = {
|
|||
<g id="g">
|
||||
<polygon
|
||||
className="vis-graphic--fill vis-graphic--fill-c"
|
||||
points="97.5,97.5 97.5,82.5 82.5,82.5 82.5,97.5 67.5,97.5 52.5,97.5 37.5,97.5 37.5,112.5 52.5,112.5
|
||||
52.5,127.5 67.5,127.5 67.5,112.5 82.5,112.5 82.5,127.5 82.5,142.5 97.5,142.5 97.5,127.5 112.5,127.5 112.5,112.5 97.5,112.5
|
||||
points="97.5,97.5 97.5,82.5 82.5,82.5 82.5,97.5 67.5,97.5 52.5,97.5 37.5,97.5 37.5,112.5 52.5,112.5
|
||||
52.5,127.5 67.5,127.5 67.5,112.5 82.5,112.5 82.5,127.5 82.5,142.5 97.5,142.5 97.5,127.5 112.5,127.5 112.5,112.5 97.5,112.5
|
||||
"
|
||||
/>
|
||||
<polygon
|
||||
className="vis-graphic--fill vis-graphic--fill-c"
|
||||
points="127.5,82.5 127.5,97.5 112.5,97.5 112.5,112.5 127.5,112.5 127.5,127.5 142.5,127.5 142.5,112.5
|
||||
points="127.5,82.5 127.5,97.5 112.5,97.5 112.5,112.5 127.5,112.5 127.5,127.5 142.5,127.5 142.5,112.5
|
||||
142.5,97.5 142.5,82.5 "
|
||||
/>
|
||||
<polygon
|
||||
className="vis-graphic--fill vis-graphic--fill-c"
|
||||
points="37.5,67.5 22.5,67.5 22.5,82.5 7.5,82.5 7.5,97.5 7.5,112.5 7.5,127.5 22.5,127.5 22.5,112.5
|
||||
points="37.5,67.5 22.5,67.5 22.5,82.5 7.5,82.5 7.5,97.5 7.5,112.5 7.5,127.5 22.5,127.5 22.5,112.5
|
||||
22.5,97.5 37.5,97.5 37.5,82.5 "
|
||||
/>
|
||||
</g>
|
||||
|
@ -117,7 +117,7 @@ const GRAPHIC_SVGS = {
|
|||
/>
|
||||
<polygon
|
||||
className="vis-graphic--fill vis-graphic--fill-a"
|
||||
points="112.5,67.5 97.5,67.5 82.5,67.5 82.5,82.5 97.5,82.5 97.5,97.5 97.5,112.5 112.5,112.5 112.5,97.5
|
||||
points="112.5,67.5 97.5,67.5 82.5,67.5 82.5,82.5 97.5,82.5 97.5,97.5 97.5,112.5 112.5,112.5 112.5,97.5
|
||||
127.5,97.5 127.5,82.5 112.5,82.5 "
|
||||
/>
|
||||
<rect
|
||||
|
|
|
@ -377,6 +377,11 @@ export const timeMachineReducer = (
|
|||
return setViewProperties(state, {yColumn})
|
||||
}
|
||||
|
||||
case 'SET_Y_SERIES_COLUMNS': {
|
||||
const {ySeriesColumns} = action.payload
|
||||
return setViewProperties(state, {ySeriesColumns})
|
||||
}
|
||||
|
||||
case 'SET_X_AXIS_LABEL': {
|
||||
const {xAxisLabel} = action.payload
|
||||
|
||||
|
@ -384,6 +389,7 @@ export const timeMachineReducer = (
|
|||
case 'histogram':
|
||||
case 'heatmap':
|
||||
case 'scatter':
|
||||
case 'mosaic':
|
||||
return setViewProperties(state, {xAxisLabel})
|
||||
default:
|
||||
return setYAxis(state, {label: xAxisLabel})
|
||||
|
@ -397,6 +403,7 @@ export const timeMachineReducer = (
|
|||
case 'histogram':
|
||||
case 'heatmap':
|
||||
case 'scatter':
|
||||
case 'mosaic':
|
||||
return setViewProperties(state, {yAxisLabel})
|
||||
default:
|
||||
return setYAxis(state, {label: yAxisLabel})
|
||||
|
|
|
@ -8,8 +8,10 @@ import {fromFlux, Table} from '@influxdata/giraffe'
|
|||
import {
|
||||
defaultXColumn,
|
||||
defaultYColumn,
|
||||
mosaicYcolumn,
|
||||
getNumericColumns as getNumericColumnsUtil,
|
||||
getGroupableColumns as getGroupableColumnsUtil,
|
||||
getStringColumns as getStringColumnsUtil,
|
||||
} from 'src/shared/utils/vis'
|
||||
import {
|
||||
getWindowPeriod,
|
||||
|
@ -102,6 +104,7 @@ export const getVisTable = (
|
|||
}
|
||||
|
||||
const getNumericColumnsMemoized = memoizeOne(getNumericColumnsUtil)
|
||||
const getStringColumnsMemoized = memoizeOne(getStringColumnsUtil)
|
||||
|
||||
export const getNumericColumns = (state: AppState): string[] => {
|
||||
const {table} = getVisTable(state)
|
||||
|
@ -109,6 +112,12 @@ export const getNumericColumns = (state: AppState): string[] => {
|
|||
return getNumericColumnsMemoized(table)
|
||||
}
|
||||
|
||||
export const getStringColumns = (state: AppState): string[] => {
|
||||
const {table} = getVisTable(state)
|
||||
|
||||
return getStringColumnsMemoized(table)
|
||||
}
|
||||
|
||||
const getGroupableColumnsMemoized = memoizeOne(getGroupableColumnsUtil)
|
||||
|
||||
export const getGroupableColumns = (state: AppState): string[] => {
|
||||
|
@ -129,7 +138,19 @@ export const getXColumnSelection = (state: AppState): string => {
|
|||
|
||||
export const getYColumnSelection = (state: AppState): string => {
|
||||
const {table} = getVisTable(state)
|
||||
const preferredYColumnKey = get(
|
||||
const tm = getActiveTimeMachine(state)
|
||||
let preferredYColumnKey
|
||||
|
||||
if (tm.view.properties.type === 'mosaic') {
|
||||
preferredYColumnKey = get(
|
||||
getActiveTimeMachine(state),
|
||||
'view.properties.ySeriesColumns[0]'
|
||||
)
|
||||
|
||||
return mosaicYcolumn(table, preferredYColumnKey)
|
||||
}
|
||||
|
||||
preferredYColumnKey = get(
|
||||
getActiveTimeMachine(state),
|
||||
'view.properties.yColumn'
|
||||
)
|
||||
|
@ -156,13 +177,37 @@ const getSymbolColumnsSelectionMemoized = memoizeOne(
|
|||
)
|
||||
|
||||
export const getFillColumnsSelection = (state: AppState): string[] => {
|
||||
const validFillColumns = getGroupableColumns(state)
|
||||
const {table} = getVisTable(state)
|
||||
const tm = getActiveTimeMachine(state)
|
||||
const graphType = tm.view.properties.type
|
||||
let validFillColumns
|
||||
if (graphType === 'mosaic') {
|
||||
validFillColumns = getStringColumnsMemoized(table)
|
||||
} else {
|
||||
validFillColumns = getGroupableColumns(state)
|
||||
}
|
||||
|
||||
const preference = get(
|
||||
getActiveTimeMachine(state),
|
||||
'view.properties.fillColumns'
|
||||
)
|
||||
|
||||
if (graphType === 'mosaic') {
|
||||
//user hasn't selected a fill column yet
|
||||
if (preference === null) {
|
||||
//check if value is a string[]
|
||||
for (const key of validFillColumns) {
|
||||
if (key.startsWith('_value')) {
|
||||
return [key]
|
||||
}
|
||||
}
|
||||
//check if value is a numeric column
|
||||
if (table.columnKeys.includes('_value')) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {fluxGroupKeyUnion} = getVisTable(state)
|
||||
|
||||
return getFillColumnsSelectionMemoized(
|
||||
|
@ -252,6 +297,17 @@ export const getSaveableView = (state: AppState): QueryView & {id?: string} => {
|
|||
}
|
||||
|
||||
// TODO: remove all of these conditionals
|
||||
if (saveableView.properties.type === 'mosaic') {
|
||||
saveableView = {
|
||||
...saveableView,
|
||||
properties: {
|
||||
...saveableView.properties,
|
||||
xColumn: getXColumnSelection(state),
|
||||
ySeriesColumns: [getYColumnSelection(state)],
|
||||
fillColumns: getFillColumnsSelection(state),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (saveableView.properties.type === 'histogram') {
|
||||
saveableView = {
|
||||
|
|
|
@ -81,6 +81,7 @@ export {
|
|||
LinePlusSingleStatProperties,
|
||||
ScatterViewProperties,
|
||||
HeatmapViewProperties,
|
||||
MosaicViewProperties,
|
||||
SingleStatViewProperties,
|
||||
HistogramViewProperties,
|
||||
GaugeViewProperties,
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
HeatmapViewProperties,
|
||||
HistogramViewProperties,
|
||||
LinePlusSingleStatProperties,
|
||||
MosaicViewProperties,
|
||||
MarkdownViewProperties,
|
||||
NewView,
|
||||
RemoteDataState,
|
||||
|
@ -260,6 +261,28 @@ const NEW_VIEW_CREATORS = {
|
|||
ySuffix: '',
|
||||
},
|
||||
}),
|
||||
mosaic: (): NewView<MosaicViewProperties> => ({
|
||||
...defaultView(),
|
||||
properties: {
|
||||
type: 'mosaic',
|
||||
shape: 'chronograf-v2',
|
||||
queries: [defaultViewQuery()],
|
||||
colors: NINETEEN_EIGHTY_FOUR,
|
||||
note: '',
|
||||
showNoteWhenEmpty: false,
|
||||
fillColumns: null,
|
||||
xColumn: null,
|
||||
xDomain: null,
|
||||
ySeriesColumns: null,
|
||||
yDomain: null,
|
||||
xAxisLabel: '',
|
||||
yAxisLabel: '',
|
||||
xPrefix: '',
|
||||
xSuffix: '',
|
||||
yPrefix: '',
|
||||
ySuffix: '',
|
||||
},
|
||||
}),
|
||||
threshold: (): NewView<CheckViewProperties> => ({
|
||||
...defaultView('check'),
|
||||
properties: {
|
||||
|
|
Loading…
Reference in New Issue