diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc6db3693..6b7b9c56ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cmd/influx/authorization.go b/cmd/influx/authorization.go index 1d65624fb0..1bc74c200b 100644 --- a/cmd/influx/authorization.go +++ b/cmd/influx/authorization.go @@ -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(), diff --git a/flags.yml b/flags.yml index 98c3dd67e9..777529ccc5 100644 --- a/flags.yml +++ b/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 diff --git a/kit/feature/list.go b/kit/feature/list.go index aa49c90d2a..1938007e72 100644 --- a/kit/feature/list.go +++ b/kit/feature/list.go @@ -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, } diff --git a/tenant/middleware_urm_auth.go b/tenant/middleware_urm_auth.go index 6e18cc8ebd..fd69675811 100644 --- a/tenant/middleware_urm_auth.go +++ b/tenant/middleware_urm_auth.go @@ -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 { diff --git a/tenant/middleware_urm_auth_test.go b/tenant/middleware_urm_auth_test.go index 2b1ca396ca..b135568e08 100644 --- a/tenant/middleware_urm_auth_test.go +++ b/tenant/middleware_urm_auth_test.go @@ -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) diff --git a/ui/cypress/e2e/communityTemplate.test.ts b/ui/cypress/e2e/communityTemplate.test.ts index a7c54dc5ca..66dbf660d4 100644 --- a/ui/cypress/e2e/communityTemplate.test.ts +++ b/ui/cypress/e2e/communityTemplate.test.ts @@ -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 }) diff --git a/ui/cypress/e2e/explorer.test.ts b/ui/cypress/e2e/explorer.test.ts index aa257f07dd..a3b10d7dfc 100644 --- a/ui/cypress/e2e/explorer.test.ts +++ b/ui/cypress/e2e/explorer.test.ts @@ -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}` + ) + } } }) diff --git a/ui/cypress/index.d.ts b/ui/cypress/index.d.ts index 372c1a08c2..209c4725f9 100644 --- a/ui/cypress/index.d.ts +++ b/ui/cypress/index.d.ts @@ -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 } } } diff --git a/ui/cypress/support/commands.ts b/ui/cypress/support/commands.ts index 53596ab1fc..99b2c58289 100644 --- a/ui/cypress/support/commands.ts +++ b/ui/cypress/support/commands.ts @@ -359,6 +359,50 @@ export const createTelegraf = ( }) } +export const createRule = ( + orgID: string, + endpointID: string, + name = '' +): Cypress.Chainable => { + 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) diff --git a/ui/src/buckets/actions/thunks.ts b/ui/src/buckets/actions/thunks.ts index bea99dcdd2..8cfd371fe5 100644 --- a/ui/src/buckets/actions/thunks.ts +++ b/ui/src/buckets/actions/thunks.ts @@ -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) { diff --git a/ui/src/notifications/rules/actions/thunks.ts b/ui/src/notifications/rules/actions/thunks.ts index 796578b4fd..1dc0c698e7 100644 --- a/ui/src/notifications/rules/actions/thunks.ts +++ b/ui/src/notifications/rules/actions/thunks.ts @@ -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) diff --git a/ui/src/resources/constants/index.ts b/ui/src/resources/constants/index.ts index ef35fbe8d6..c05a10a5bf 100644 --- a/ui/src/resources/constants/index.ts +++ b/ui/src/resources/constants/index.ts @@ -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 diff --git a/ui/src/shared/components/MosaicPlot.tsx b/ui/src/shared/components/MosaicPlot.tsx new file mode 100644 index 0000000000..26f96d0f85 --- /dev/null +++ b/ui/src/shared/components/MosaicPlot.tsx @@ -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 = ({ + 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 + } + + 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 diff --git a/ui/src/shared/components/ViewSwitcher.tsx b/ui/src/shared/components/ViewSwitcher.tsx index c91d540783..d410fc54e2 100644 --- a/ui/src/shared/components/ViewSwitcher.tsx +++ b/ui/src/shared/components/ViewSwitcher.tsx @@ -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 = ({ ) + case 'mosaic': + return ( + + {config => } + + ) + case 'scatter': return ( { 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] } diff --git a/ui/src/shared/utils/vis.ts b/ui/src/shared/utils/vis.ts index a579a0acbc..fc8925b2ec 100644 --- a/ui/src/shared/utils/vis.ts +++ b/ui/src/shared/utils/vis.ts @@ -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] diff --git a/ui/src/templates/components/CommunityTemplateHumanReadableResource.tsx b/ui/src/templates/components/CommunityTemplateHumanReadableResource.tsx new file mode 100644 index 0000000000..a846231445 --- /dev/null +++ b/ui/src/templates/components/CommunityTemplateHumanReadableResource.tsx @@ -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 + +type Props = ComponentProps & ReduxProps + +const CommunityTemplateHumanReadableResourceUnconnected: FC = ({ + link, + metaName, + resourceID, + allResources, +}) => { + const matchingResource = allResources.find( + resource => resource.id === resourceID + ) + + const humanName = matchingResource ? matchingResource.name : metaName + + return ( + + {humanName} + + ) +} + +const mstp = (state: AppState) => { + const labels = getAll