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

pull/19446/head
Stuart Carnie 2020-08-14 12:37:41 -07:00
commit b3734695c5
No known key found for this signature in database
GPG Key ID: 848D9C9718D78B4F
33 changed files with 1233 additions and 226 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;</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'}}>
&nbsp;
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,10 @@ export const VIS_TYPES: VisType[] = [
type: 'heatmap',
name: 'Heatmap',
},
{
type: 'mosaic',
name: 'Mosaic',
},
{
type: 'histogram',
name: 'Histogram',

View File

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

View File

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

View File

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

View File

@ -81,6 +81,7 @@ export {
LinePlusSingleStatProperties,
ScatterViewProperties,
HeatmapViewProperties,
MosaicViewProperties,
SingleStatViewProperties,
HistogramViewProperties,
GaugeViewProperties,

View File

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