diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b701ac3ce3..9356f66c84 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.3.0 +current_version = 1.4.3.1 files = README.md server/swagger.json parse = (?P\d+)\.(?P\d+)\.(?P\d+)\.(?P\d+) serialize = {major}.{minor}.{patch}.{release} diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 2ed630156a..71061652ba 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -1,13 +1,10 @@ +Closes # + +_Briefly describe your proposed changes:_ +_What was the problem?_ +_What was the solution?_ - [ ] CHANGELOG.md updated with a link to the PR (not the Issue) - [ ] Rebased/mergable - [ ] Tests pass - - [ ] Sign [CLA](https://influxdata.com/community/cla/) (if not already signed) - -Connect # - -### The problem - -### The Solution - - + - [ ] Sign [CLA](https://influxdata.com/community/cla/) (if not already signed) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 067cdb6ade..b6edf91840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ### Features 1. [#2526](https://github.com/influxdata/chronograf/pull/2526): Add support for RS256/JWKS verification, support for id_token parsing (as in ADFS) +1. [#3060](https://github.com/influxdata/chronograf/pull/3060): Add ability to set a color palette for Line, Stacked, Step-Plot, and Bar graphs 1. [#3103](https://github.com/influxdata/chronograf/pull/3103): Add ability to clone dashboards +1. [#3080](https://github.com/influxdata/chronograf/pull/3080): Add tabular data visualization option with features ### UI Improvements @@ -14,8 +16,15 @@ ### Bug Fixes 1. [#2950](https://github.com/influxdata/chronograf/pull/2094): Always save template variables on first edit -1. [#3101](https://github.com/influxdata/chronograf/pull/3101): Fix template variables not loading 1. [#3109](https://github.com/influxdata/chronograf/pull/3109): Display link to configure Kapacitor on Alerts Page if no configured kapacitor. +1. [#3111](https://github.com/influxdata/chronograf/pull/3111): Fix saving of new TICKscripts + +## v1.4.3.1 [2018-04-02] +### Bug Fixes + +1. [#3107](https://github.com/influxdata/chronograf/pull/3107): Fixes template variable editing not allowing saving +1. [#3094](https://github.com/influxdata/chronograf/pull/3094): Save template variables on first edit +1. [#3101](https://github.com/influxdata/chronograf/pull/3101): Fix template variables not loading all values ## v1.4.3.0 [2018-3-28] diff --git a/README.md b/README.md index a8c657d5c3..78ea39c33f 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ option. ## Versions The most recent version of Chronograf is -[v1.4.3.0](https://www.influxdata.com/downloads/). +[v1.4.3.1](https://www.influxdata.com/downloads/). Spotted a bug or have a feature request? Please open [an issue](https://github.com/influxdata/chronograf/issues/new)! @@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`. To get started right away with Docker, you can pull down our latest release: ```sh -docker pull chronograf:1.4.3.0 +docker pull chronograf:1.4.3.1 ``` ### From Source diff --git a/server/cells.go b/server/cells.go index 181095478d..b8fabf2a45 100644 --- a/server/cells.go +++ b/server/cells.go @@ -113,7 +113,7 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error { // HasCorrectColors verifies that the format of each color is correct func HasCorrectColors(c *chronograf.DashboardCell) error { for _, color := range c.CellColors { - if !oneOf(color.Type, "max", "min", "threshold", "text", "background") { + if !oneOf(color.Type, "max", "min", "threshold", "text", "background", "scale") { return chronograf.ErrInvalidColorType } if len(color.Hex) != 7 { diff --git a/server/sources.go b/server/sources.go index b6af66ce03..ddb7eac76b 100644 --- a/server/sources.go +++ b/server/sources.go @@ -635,7 +635,7 @@ type sourceUsersResponse struct { } func (r *sourceUserRequest) ValidUpdate() error { - if r.Password == "" && len(r.Permissions) == 0 && len(r.Roles) == 0 { + if r.Password == "" && r.Permissions == nil && r.Roles == nil { return fmt.Errorf("No fields to update") } return validPermissions(&r.Permissions) diff --git a/server/swagger.json b/server/swagger.json index 508ee2be0e..e8208779b0 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3,7 +3,7 @@ "info": { "title": "Chronograf", "description": "API endpoints for Chronograf", - "version": "1.4.3.0" + "version": "1.4.3.1" }, "schemes": ["http"], "basePath": "/chronograf/v1", diff --git a/ui/package.json b/ui/package.json index c9854a3050..19a8274f81 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "chronograf-ui", - "version": "1.4.3-0", + "version": "1.4.3-1", "private": false, "license": "AGPL-3.0", "description": "", @@ -116,6 +116,7 @@ "axios": "^0.13.1", "bignumber.js": "^4.0.2", "calculate-size": "^1.1.1", + "chroma-js": "^1.3.6", "classnames": "^2.2.3", "dygraphs": "2.1.0", "eslint-plugin-babel": "^4.1.2", @@ -147,4 +148,4 @@ "rome": "^2.1.22", "uuid": "^3.2.1" } -} \ No newline at end of file +} diff --git a/ui/src/admin/components/UserPermissionsDropdown.tsx b/ui/src/admin/components/UserPermissionsDropdown.tsx new file mode 100644 index 0000000000..d3345341ef --- /dev/null +++ b/ui/src/admin/components/UserPermissionsDropdown.tsx @@ -0,0 +1,66 @@ +import React, {PureComponent} from 'react' +import classnames from 'classnames' +import _ from 'lodash' + +import MultiSelectDropdown from 'src/shared/components/MultiSelectDropdown' + +import {USERS_TABLE} from 'src/admin/constants/tableSizing' +import {User} from 'src/types/influxAdmin' + +interface Props { + user: User + allPermissions: string[] + onUpdatePermissions: (user: User, permissions: any[]) => void +} + +class UserPermissionsDropdown extends PureComponent { + public render() { + return ( + + ) + } + + private handleUpdatePermissions = (permissions): void => { + const {onUpdatePermissions, user} = this.props + const allowed = permissions.map(p => p.name) + onUpdatePermissions(user, [{scope: 'all', allowed}]) + } + + private get allPermissions() { + return this.props.allPermissions.map(p => ({name: p})) + } + + private get userPermissions() { + return _.get(this.props.user, ['permissions', '0', 'allowed'], []) + } + + private get selectedPermissions() { + return this.userPermissions.map(p => ({name: p})) + } + + private get permissionsLabel() { + const {user} = this.props + if (user.permissions && user.permissions.length) { + return 'Select Permissions' + } + + return '' + } + + private get permissionsClass() { + return classnames(`dropdown-${USERS_TABLE.colPermissions}`, { + 'admin-table--multi-select-empty': !this.props.user.permissions.length, + }) + } +} + +export default UserPermissionsDropdown diff --git a/ui/src/admin/components/UserRoleDropdown.tsx b/ui/src/admin/components/UserRoleDropdown.tsx new file mode 100644 index 0000000000..e287934051 --- /dev/null +++ b/ui/src/admin/components/UserRoleDropdown.tsx @@ -0,0 +1,58 @@ +import React, {PureComponent} from 'react' +import classnames from 'classnames' + +import _ from 'lodash' + +import MultiSelectDropdown from 'src/shared/components/MultiSelectDropdown' + +import {USERS_TABLE} from 'src/admin/constants/tableSizing' +import {User, UserRole} from 'src/types/influxAdmin' + +interface Props { + user: User + allRoles: any[] + onUpdateRoles: (user: User, roles: UserRole[]) => void +} + +class UserRoleDropdown extends PureComponent { + public render() { + const {allRoles} = this.props + + return ( + + ) + } + + private handleUpdateRoles = (roleNames): void => { + const {user, allRoles, onUpdateRoles} = this.props + const roles = allRoles.filter(r => roleNames.find(rn => rn.name === r.name)) + + onUpdateRoles(user, roles) + } + + private get roles(): UserRole[] { + const roles = _.get(this.props.user, 'roles', []) as UserRole[] + return roles.map(({name}) => ({name})) + } + + private get rolesClass(): string { + return classnames(`dropdown-${USERS_TABLE.colRoles}`, { + 'admin-table--multi-select-empty': !this.roles.length, + }) + } + + private get rolesLabel(): string { + return this.roles.length ? '' : 'Select Roles' + } +} + +export default UserRoleDropdown diff --git a/ui/src/admin/components/UserRow.js b/ui/src/admin/components/UserRow.js deleted file mode 100644 index fa6a744cc3..0000000000 --- a/ui/src/admin/components/UserRow.js +++ /dev/null @@ -1,169 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import _ from 'lodash' -import classnames from 'classnames' - -import UserEditName from 'src/admin/components/UserEditName' -import UserNewPassword from 'src/admin/components/UserNewPassword' -import MultiSelectDropdown from 'shared/components/MultiSelectDropdown' -import ConfirmOrCancel from 'shared/components/ConfirmOrCancel' -import ConfirmButton from 'shared/components/ConfirmButton' -import ChangePassRow from 'src/admin/components/ChangePassRow' -import {USERS_TABLE} from 'src/admin/constants/tableSizing' - -const UserRow = ({ - user: {name, roles = [], permissions, password}, - user, - allRoles, - allPermissions, - hasRoles, - isNew, - isEditing, - onEdit, - onSave, - onCancel, - onDelete, - onUpdatePermissions, - onUpdateRoles, - onUpdatePassword, -}) => { - function handleUpdatePermissions(perms) { - const allowed = perms.map(p => p.name) - onUpdatePermissions(user, [{scope: 'all', allowed}]) - } - - function handleUpdateRoles(roleNames) { - onUpdateRoles( - user, - allRoles.filter(r => roleNames.find(rn => rn.name === r.name)) - ) - } - - function handleUpdatePassword() { - onUpdatePassword(user, password) - } - - const perms = _.get(permissions, ['0', 'allowed'], []) - - const wrappedDelete = () => { - onDelete(user) - } - - if (isEditing) { - return ( - - - - {hasRoles ? -- : null} - -- - - - - - ) - } - - return ( - - {name} - - - - {hasRoles ? ( - - ({name: r.name}))} - label={roles.length ? '' : 'Select Roles'} - onApply={handleUpdateRoles} - buttonSize="btn-xs" - buttonColor="btn-primary" - customClass={classnames(`dropdown-${USERS_TABLE.colRoles}`, { - 'admin-table--multi-select-empty': !roles.length, - })} - resetStateOnReceiveProps={false} - /> - - ) : null} - - {allPermissions && allPermissions.length ? ( - ({name: p}))} - selectedItems={perms.map(p => ({name: p}))} - label={ - permissions && permissions.length ? '' : 'Select Permissions' - } - onApply={handleUpdatePermissions} - buttonSize="btn-xs" - buttonColor="btn-primary" - customClass={classnames(`dropdown-${USERS_TABLE.colPermissions}`, { - 'admin-table--multi-select-empty': !permissions.length, - })} - resetStateOnReceiveProps={false} - /> - ) : null} - - - - - - ) -} - -const {arrayOf, bool, func, shape, string} = PropTypes - -UserRow.propTypes = { - user: shape({ - name: string, - roles: arrayOf( - shape({ - name: string, - }) - ), - permissions: arrayOf( - shape({ - name: string, - }) - ), - password: string, - }).isRequired, - allRoles: arrayOf(shape()), - allPermissions: arrayOf(string), - hasRoles: bool, - isNew: bool, - isEditing: bool, - onCancel: func, - onEdit: func, - onSave: func, - onDelete: func.isRequired, - onUpdatePermissions: func, - onUpdateRoles: func, - onUpdatePassword: func, -} - -export default UserRow diff --git a/ui/src/admin/components/UserRow.tsx b/ui/src/admin/components/UserRow.tsx new file mode 100644 index 0000000000..a2e908c6c6 --- /dev/null +++ b/ui/src/admin/components/UserRow.tsx @@ -0,0 +1,120 @@ +import React, {PureComponent} from 'react' + +import UserPermissionsDropdown from 'src/admin/components/UserPermissionsDropdown' +import UserRoleDropdown from 'src/admin/components/UserRoleDropdown' +import ChangePassRow from 'src/admin/components/ChangePassRow' +import ConfirmButton from 'src/shared/components/ConfirmButton' +import {USERS_TABLE} from 'src/admin/constants/tableSizing' + +import UserRowEdit from 'src/admin/components/UserRowEdit' +import {User} from 'src/types/influxAdmin' + +interface UserRowProps { + user: User + allRoles: any[] + allPermissions: string[] + hasRoles: boolean + isNew: boolean + isEditing: boolean + onCancel: () => void + onEdit: () => void + onSave: () => void + onDelete: (user: User) => void + onUpdatePermissions: (user: User, permissions: any[]) => void + onUpdateRoles: (user: User, roles: any[]) => void + onUpdatePassword: (user: User, password: string) => void +} + +class UserRow extends PureComponent { + public render() { + const { + user, + allRoles, + allPermissions, + hasRoles, + isNew, + isEditing, + onEdit, + onSave, + onCancel, + onUpdatePermissions, + onUpdateRoles, + } = this.props + + if (isEditing) { + return ( + + ) + } + + return ( + + {user.name} + + + + {hasRoles && ( + + + + )} + + {this.hasPermissions && ( + + )} + + + + + + ) + } + + private handleDelete = (): void => { + const {user, onDelete} = this.props + + onDelete(user) + } + + private handleUpdatePassword = (): void => { + const {user, onUpdatePassword} = this.props + + onUpdatePassword(user, user.password) + } + + private get hasPermissions() { + const {allPermissions} = this.props + return allPermissions && !!allPermissions.length + } +} + +export default UserRow diff --git a/ui/src/admin/components/UserRowEdit.tsx b/ui/src/admin/components/UserRowEdit.tsx new file mode 100644 index 0000000000..60d8516038 --- /dev/null +++ b/ui/src/admin/components/UserRowEdit.tsx @@ -0,0 +1,47 @@ +import React, {SFC} from 'react' +import UserEditName from 'src/admin/components/UserEditName' +import UserNewPassword from 'src/admin/components/UserNewPassword' +import ConfirmOrCancel from 'src/shared/components/ConfirmOrCancel' +import {USERS_TABLE} from 'src/admin/constants/tableSizing' + +import {User} from 'src/types/influxAdmin' + +interface UserRowEditProps { + user: User + onEdit: () => void + onSave: () => void + onCancel: () => void + isNew: boolean + hasRoles: boolean +} + +const UserRowEdit: SFC = ({ + user, + onEdit, + onSave, + onCancel, + isNew, + hasRoles, +}) => ( + + + + {hasRoles ? -- : null} + -- + + + + +) + +export default UserRowEdit diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js index 34b4a5354e..f9efe0de08 100644 --- a/ui/src/dashboards/actions/cellEditorOverlay.js +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -57,3 +57,10 @@ export const updateTableOptions = tableOptions => ({ tableOptions, }, }) + +export const updateLineColors = lineColors => ({ + type: 'UPDATE_LINE_COLORS', + payload: { + lineColors, + }, +}) diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index c0e5019a7d..9b04de4659 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -7,6 +7,7 @@ import OptIn from 'shared/components/OptIn' import Input from 'src/dashboards/components/DisplayOptionsInput' import {Tabber, Tab} from 'src/dashboards/components/Tabber' import FancyScrollbar from 'shared/components/FancyScrollbar' +import LineGraphColorSelector from 'src/shared/components/LineGraphColorSelector' import { AXES_SCALE_OPTIONS, @@ -102,6 +103,7 @@ class AxesOptions extends Component { type="text" /> +
{ const {queriesWorkingDraft, staticLegend} = this.state - const {cell, thresholdsListColors, gaugeColors} = this.props + const {cell, thresholdsListColors, gaugeColors, lineColors} = this.props const queries = queriesWorkingDraft.map(q => { const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} @@ -120,20 +121,12 @@ class CellEditorOverlay extends Component { } }) - let colors = [] - - switch (cell.type) { - case 'gauge': { - colors = stringifyColorValues(gaugeColors) - break - } - case 'single-stat': - case 'line-plus-single-stat': - case 'table': { - colors = stringifyColorValues(thresholdsListColors) - break - } - } + const colors = getCellTypeColors({ + cellType: cell.type, + gaugeColors, + thresholdsListColors, + lineColors, + }) this.props.onSave({ ...cell, @@ -390,8 +383,9 @@ CellEditorOverlay.propTypes = { dashboardID: string.isRequired, sources: arrayOf(shape()), thresholdsListType: string.isRequired, - thresholdsListColors: arrayOf(shape({}).isRequired).isRequired, - gaugeColors: arrayOf(shape({}).isRequired).isRequired, + thresholdsListColors: colorsNumberSchema.isRequired, + gaugeColors: colorsNumberSchema.isRequired, + lineColors: colorsStringSchema.isRequired, } CEOBottom.propTypes = { diff --git a/ui/src/dashboards/components/GaugeOptions.js b/ui/src/dashboards/components/GaugeOptions.js index 3197464465..a4b2d4abe0 100644 --- a/ui/src/dashboards/components/GaugeOptions.js +++ b/ui/src/dashboards/components/GaugeOptions.js @@ -20,6 +20,7 @@ import { updateGaugeColors, updateAxes, } from 'src/dashboards/actions/cellEditorOverlay' +import {colorsNumberSchema} from 'shared/schemas' class GaugeOptions extends Component { handleAddThreshold = () => { @@ -219,18 +220,10 @@ class GaugeOptions extends Component { } } -const {arrayOf, func, number, shape, string} = PropTypes +const {func, shape} = PropTypes GaugeOptions.propTypes = { - gaugeColors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }).isRequired - ), + gaugeColors: colorsNumberSchema, handleUpdateGaugeColors: func.isRequired, handleUpdateAxes: func.isRequired, axes: shape({}).isRequired, diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index d84a76ed06..5fe725856f 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -6,7 +6,8 @@ import RefreshingGraph from 'src/shared/components/RefreshingGraph' import buildQueries from 'utils/buildQueriesForGraphs' import VisualizationName from 'src/dashboards/components/VisualizationName' -import {stringifyColorValues} from 'src/shared/constants/colorOperations' +import {getCellTypeColors} from 'src/dashboards/constants/cellEditor' +import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas' const DashVisualization = ( { @@ -14,6 +15,7 @@ const DashVisualization = ( type, templates, timeRange, + lineColors, autoRefresh, gaugeColors, queryConfigs, @@ -26,14 +28,19 @@ const DashVisualization = ( }, {source: {links: {proxy}}} ) => { - const colors = type === 'gauge' ? gaugeColors : thresholdsListColors + const colors = getCellTypeColors({ + cellType: type, + gaugeColors, + thresholdsListColors, + lineColors, + }) return (
({ gaugeColors, thresholdsListColors, + lineColors, type, axes, tableOptions, diff --git a/ui/src/dashboards/components/template_variables/Row.js b/ui/src/dashboards/components/template_variables/Row.js index 4242f0075c..3be354f73e 100644 --- a/ui/src/dashboards/components/template_variables/Row.js +++ b/ui/src/dashboards/components/template_variables/Row.js @@ -70,7 +70,7 @@ const TemplateVariableRow = ({ t.type === selectedType).text} className="dropdown-140" /> @@ -84,7 +84,7 @@ const TemplateVariableRow = ({ selectedMeasurement={selectedMeasurement} selectedTagKey={selectedTagKey} onSelectTagKey={onSelectTagKey} - onStartEdit={onStartEdit} + onStartEdit={onStartEdit('tempVar')} onErrorThrown={onErrorThrown} /> { switch (cellType) { @@ -18,3 +19,33 @@ export const AXES_SCALE_OPTIONS = { export const TOOLTIP_Y_VALUE_FORMAT = '

K/M/B = Thousand / Million / Billion
K/M/G = Kilo / Mega / Giga

' + +export const getCellTypeColors = ({ + cellType, + gaugeColors, + thresholdsListColors, + lineColors, +}) => { + let colors = [] + + switch (cellType) { + case 'gauge': { + colors = stringifyColorValues(gaugeColors) + break + } + case 'single-stat': + case 'line-plus-single-stat': + case 'table': { + colors = stringifyColorValues(thresholdsListColors) + break + } + case 'bar': + case 'line': + case 'line-stacked': + case 'line-stepplot': { + colors = stringifyColorValues(lineColors) + } + } + + return colors +} diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 7a9c73ada2..45091183af 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -37,6 +37,7 @@ import { import {presentationButtonDispatcher} from 'shared/dispatchers' import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants' import {notifyDashboardNotFound} from 'shared/copy/notifications' +import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas' const FORMAT_INFLUXQL = 'influxql' const defaultTimeRange = { @@ -283,6 +284,7 @@ class DashboardPage extends Component { showTemplateControlBar, dashboard, dashboards, + lineColors, gaugeColors, autoRefresh, selectedCell, @@ -399,6 +401,7 @@ class DashboardPage extends Component { thresholdsListType={thresholdsListType} thresholdsListColors={thresholdsListColors} gaugeColors={gaugeColors} + lineColors={lineColors} /> ) : null} { @@ -549,6 +553,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { thresholdsListType, thresholdsListColors, gaugeColors, + lineColors, }, } = state @@ -579,6 +584,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { thresholdsListType, thresholdsListColors, gaugeColors, + lineColors, } } diff --git a/ui/src/dashboards/graphics/graph.js b/ui/src/dashboards/graphics/graph.js index 4038517bb8..8473a9f5b6 100644 --- a/ui/src/dashboards/graphics/graph.js +++ b/ui/src/dashboards/graphics/graph.js @@ -545,10 +545,9 @@ export const GRAPH_TYPES = [ menuOption: 'Gauge', graphic: GRAPH_SVGS.gauge, }, - // FEATURE FLAG for Table-Graph - // { - // type: 'table', - // menuOption: 'Table', - // graphic: GRAPH_SVGS.table, - // }, + { + type: 'table', + menuOption: 'Table', + graphic: GRAPH_SVGS.table, + }, ] diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js index 4629e89750..d51d2c503c 100644 --- a/ui/src/dashboards/reducers/cellEditorOverlay.js +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -9,6 +9,11 @@ import { getThresholdsListType, } from 'shared/constants/thresholds' +import { + DEFAULT_LINE_COLORS, + validateLineColors, +} from 'src/shared/constants/graphColorPalettes' + import {initializeOptions} from 'src/dashboards/constants/cellEditor' export const initialState = { @@ -16,6 +21,7 @@ export const initialState = { thresholdsListType: THRESHOLD_TYPE_TEXT, thresholdsListColors: DEFAULT_THRESHOLDS_LIST_COLORS, gaugeColors: DEFAULT_GAUGE_COLORS, + lineColors: DEFAULT_LINE_COLORS, } export default function cellEditorOverlay(state = initialState, action) { @@ -36,12 +42,15 @@ export default function cellEditorOverlay(state = initialState, action) { initializeOptions('table') ) + const lineColors = validateLineColors(colors) + return { ...state, cell: {...cell, tableOptions}, thresholdsListType, thresholdsListColors, gaugeColors, + lineColors, } } @@ -101,6 +110,12 @@ export default function cellEditorOverlay(state = initialState, action) { return {...state, cell} } + + case 'UPDATE_LINE_COLORS': { + const {lineColors} = action.payload + + return {...state, lineColors} + } } return state diff --git a/ui/src/dashboards/utils/templateVariableQueryGenerator.js b/ui/src/dashboards/utils/templateVariableQueryGenerator.js index 163b72102c..d15a43be7d 100644 --- a/ui/src/dashboards/utils/templateVariableQueryGenerator.js +++ b/ui/src/dashboards/utils/templateVariableQueryGenerator.js @@ -55,8 +55,8 @@ const generateTemplateVariableQuery = ({ export const makeQueryForTemplate = ({influxql, db, measurement, tagKey}) => influxql - .replace(':database:', db) - .replace(':measurement:', measurement) - .replace(':tagKey:', tagKey) + .replace(':database:', `"${db}"`) + .replace(':measurement:', `"${measurement}"`) + .replace(':tagKey:', `"${tagKey}"`) export default generateTemplateVariableQuery diff --git a/ui/src/data_explorer/components/VisView.js b/ui/src/data_explorer/components/VisView.js index 2f71af1ad0..b9359d8584 100644 --- a/ui/src/data_explorer/components/VisView.js +++ b/ui/src/data_explorer/components/VisView.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import Table from './Table' import RefreshingGraph from 'shared/components/RefreshingGraph' +import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes' const VisView = ({ axes, @@ -37,6 +38,7 @@ const VisView = ({ return (
diff --git a/ui/src/kapacitor/components/TickscriptEditorControls.js b/ui/src/kapacitor/components/TickscriptEditorControls.js index f57724c8e0..116757b1b5 100644 --- a/ui/src/kapacitor/components/TickscriptEditorControls.js +++ b/ui/src/kapacitor/components/TickscriptEditorControls.js @@ -19,7 +19,7 @@ const TickscriptEditorControls = ({ {isNewTickscript ? ( ) : ( - + )}
diff --git a/ui/src/kapacitor/containers/TickscriptPage.js b/ui/src/kapacitor/containers/TickscriptPage.js index 959f07db48..3b645e838c 100644 --- a/ui/src/kapacitor/containers/TickscriptPage.js +++ b/ui/src/kapacitor/containers/TickscriptPage.js @@ -173,6 +173,7 @@ class TickscriptPage extends Component { response = await updateTask(kapacitor, task, ruleID, router, sourceID) } else { response = await createTask(kapacitor, task, router, sourceID) + router.push(`/sources/${sourceID}/tickscript/${response.id}`) } if (response.code) { this.setState({unsavedChanges: true, consoleMessage: response.message}) @@ -227,6 +228,7 @@ class TickscriptPage extends Component { unsavedChanges, consoleMessage, } = this.state + return ( { + const {disabled} = this.props + + if (disabled) { + return + } + this.setState({expanded: !this.state.expanded}) + } + + handleClickOutside = () => { + this.setState({expanded: false}) + } + + handleDropdownClick = colorScale => () => { + this.props.onChoose(colorScale) + this.setState({expanded: false}) + } + + generateGradientStyle = colors => ({ + background: `linear-gradient(to right, ${colors[0].hex} 0%,${ + colors[1].hex + } 50%,${colors[2].hex} 100%)`, + }) + + render() { + const {expanded} = this.state + const {selected, disabled, stretchToFit} = this.props + + const dropdownClassNames = classnames('color-dropdown', { + open: expanded, + 'color-dropdown--stretch': stretchToFit, + }) + const toggleClassNames = classnames( + 'btn btn-sm btn-default color-dropdown--toggle', + {active: expanded, 'color-dropdown__disabled': disabled} + ) + + return ( +
+
+
+
{selected[0].name}
+ +
+ {expanded ? ( +
+ + {LINE_COLOR_SCALES.map(colorScale => ( +
+
+ + {colorScale.name} + +
+ ))} + +
+ ) : null} +
+ ) + } +} + +const {arrayOf, bool, func, shape, string} = PropTypes + +ColorScaleDropdown.propTypes = { + selected: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + }).isRequired + ).isRequired, + onChoose: func.isRequired, + stretchToFit: bool, + disabled: bool, +} + +export default OnClickOutside(ColorScaleDropdown) diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index 59db5ffc0c..4ab51dfe1b 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -23,11 +23,17 @@ import { LABEL_WIDTH, CHAR_PIXELS, barPlotter, - hasherino, highlightSeriesOpts, } from 'src/shared/graphs/helpers' + +import { + DEFAULT_LINE_COLORS, + getLineColorsHexes, +} from 'src/shared/constants/graphColorPalettes' const {LINEAR, LOG, BASE_10, BASE_2} = AXES_SCALE_OPTIONS +import {colorsStringSchema} from 'shared/schemas' + class Dygraph extends Component { constructor(props) { super(props) @@ -53,7 +59,7 @@ class Dygraph extends Component { fillGraph, logscale: y.scale === LOG, colors: this.getLineColors(), - series: this.hashColorDygraphSeries(), + series: this.colorDygraphSeries(), plugins: [new Dygraphs.Plugins.Crosshair({direction: 'vertical'})], axes: { y: { @@ -149,7 +155,7 @@ class Dygraph extends Component { }, }, colors: this.getLineColors(), - series: this.hashColorDygraphSeries(), + series: this.colorDygraphSeries(), plotter: isBarGraph ? barPlotter : null, drawCallback: this.annotationsRef.heartbeat, } @@ -189,6 +195,33 @@ class Dygraph extends Component { onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper)) } + colorDygraphSeries = () => { + const {dygraphSeries, children, colors, overrideLineColors} = this.props + const numSeries = Object.keys(dygraphSeries).length + + let lineColors = getLineColorsHexes(colors, numSeries) + + if (React.children && React.children.count(children)) { + // If graph is line-plus-single-stat then reserve colors for single stat + lineColors = getLineColorsHexes(DEFAULT_LINE_COLORS, numSeries) + } + + if (overrideLineColors) { + lineColors = getLineColorsHexes(overrideLineColors, numSeries) + } + + const coloredDygraphSeries = {} + + for (const seriesName in dygraphSeries) { + const series = dygraphSeries[seriesName] + const color = lineColors[Object.keys(dygraphSeries).indexOf(seriesName)] + + coloredDygraphSeries[seriesName] = {...series, color} + } + + return coloredDygraphSeries + } + eventToTimestamp = ({pageX: pxBetweenMouseAndPage}) => { const {left: pxBetweenGraphAndPage} = this.graphRef.getBoundingClientRect() const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage @@ -213,21 +246,6 @@ class Dygraph extends Component { this.setState({isHoveringThisGraph: false}) } - hashColorDygraphSeries = () => { - const {dygraphSeries} = this.props - const colors = this.getLineColors() - const hashColorDygraphSeries = {} - - for (const seriesName in dygraphSeries) { - const series = dygraphSeries[seriesName] - const hashIndex = hasherino(seriesName, colors.length) - const color = colors[hashIndex] - hashColorDygraphSeries[seriesName] = {...series, color} - } - - return hashColorDygraphSeries - } - handleHideLegend = e => { const {top, bottom, left, right} = this.graphRef.getBoundingClientRect() @@ -363,7 +381,7 @@ class Dygraph extends Component { /> {staticLegend && ( ({ diff --git a/ui/src/shared/components/Gauge.js b/ui/src/shared/components/Gauge.js index 52ea1614eb..4d1e3b5692 100644 --- a/ui/src/shared/components/Gauge.js +++ b/ui/src/shared/components/Gauge.js @@ -10,6 +10,8 @@ import { MIN_THRESHOLDS, } from 'shared/constants/thresholds' +import {colorsStringSchema} from 'shared/schemas' + class Gauge extends Component { constructor(props) { super(props) @@ -325,21 +327,13 @@ class Gauge extends Component { } } -const {arrayOf, number, shape, string} = PropTypes +const {number, string} = PropTypes Gauge.propTypes = { width: string.isRequired, height: string.isRequired, gaugePosition: number.isRequired, - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ).isRequired, + colors: colorsStringSchema.isRequired, prefix: string.isRequired, suffix: string.isRequired, } diff --git a/ui/src/shared/components/GaugeChart.js b/ui/src/shared/components/GaugeChart.js index 0516d9c11d..cad6d44280 100644 --- a/ui/src/shared/components/GaugeChart.js +++ b/ui/src/shared/components/GaugeChart.js @@ -7,6 +7,8 @@ import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds' import {stringifyColorValues} from 'src/shared/constants/colorOperations' import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants' +import {colorsStringSchema} from 'shared/schemas' + class GaugeChart extends PureComponent { render() { const { @@ -82,15 +84,7 @@ GaugeChart.propTypes = { cellHeight: number, resizerTopHeight: number, resizeCoords: shape(), - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ), + colors: colorsStringSchema, prefix: string.isRequired, suffix: string.isRequired, } diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index 18ade5ada1..654593492a 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -8,6 +8,8 @@ import {IS_STATIC_LEGEND} from 'src/shared/constants' import _ from 'lodash' +import {colorsStringSchema} from 'shared/schemas' + const getSource = (cell, source, sources, defaultSource) => { const s = _.get(cell, ['queries', '0', 'source'], null) if (!s) { @@ -134,15 +136,7 @@ const propTypes = { i: string.isRequired, name: string.isRequired, type: string.isRequired, - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ), + colors: colorsStringSchema, }).isRequired, templates: arrayOf(shape()), host: string, diff --git a/ui/src/shared/components/LineGraph.js b/ui/src/shared/components/LineGraph.js index 25905e3173..ffab2cfc2d 100644 --- a/ui/src/shared/components/LineGraph.js +++ b/ui/src/shared/components/LineGraph.js @@ -6,7 +6,7 @@ import shallowCompare from 'react-addons-shallow-compare' import SingleStat from 'src/shared/components/SingleStat' import timeSeriesToDygraph from 'utils/timeSeriesTransformers' -import {SINGLE_STAT_LINE_COLORS} from 'src/shared/graphs/helpers' +import {colorsStringSchema} from 'shared/schemas' class LineGraph extends Component { constructor(props) { @@ -85,10 +85,6 @@ class LineGraph extends Component { connectSeparatedPoints: true, } - const lineColors = showSingleStat - ? SINGLE_STAT_LINE_COLORS - : overrideLineColors - const containerStyle = { width: 'calc(100% - 32px)', height: 'calc(100% - 16px)', @@ -118,7 +114,8 @@ class LineGraph extends Component { resizeCoords={resizeCoords} dygraphSeries={dygraphSeries} setResolution={setResolution} - overrideLineColors={lineColors} + colors={colors} + overrideLineColors={overrideLineColors} containerStyle={containerStyle} staticLegend={staticLegend} isGraphFilled={showSingleStat ? false : isGraphFilled} @@ -201,15 +198,7 @@ LineGraph.propTypes = { resizeCoords: shape(), queries: arrayOf(shape({}).isRequired).isRequired, data: arrayOf(shape({}).isRequired).isRequired, - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ), + colors: colorsStringSchema, } export default LineGraph diff --git a/ui/src/shared/components/LineGraphColorSelector.js b/ui/src/shared/components/LineGraphColorSelector.js new file mode 100644 index 0000000000..50ca3c838d --- /dev/null +++ b/ui/src/shared/components/LineGraphColorSelector.js @@ -0,0 +1,52 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import ColorScaleDropdown from 'shared/components/ColorScaleDropdown' + +import {updateLineColors} from 'src/dashboards/actions/cellEditorOverlay' +import {colorsStringSchema} from 'shared/schemas' + +class LineGraphColorSelector extends Component { + handleSelectColors = colorScale => { + const {handleUpdateLineColors} = this.props + const {colors} = colorScale + + handleUpdateLineColors(colors) + } + + render() { + const {lineColors} = this.props + + return ( +
+ + +
+ ) + } +} + +const {func} = PropTypes + +LineGraphColorSelector.propTypes = { + lineColors: colorsStringSchema.isRequired, + handleUpdateLineColors: func.isRequired, +} + +const mapStateToProps = ({cellEditorOverlay: {lineColors}}) => ({ + lineColors, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateLineColors: bindActionCreators(updateLineColors, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)( + LineGraphColorSelector +) diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 3468329eea..c3597b618d 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -9,6 +9,8 @@ import SingleStat from 'shared/components/SingleStat' import GaugeChart from 'shared/components/GaugeChart' import TableGraph from 'shared/components/TableGraph' +import {colorsStringSchema} from 'shared/schemas' + const RefreshingLineGraph = AutoRefresh(LineGraph) const RefreshingSingleStat = AutoRefresh(SingleStat) const RefreshingGaugeChart = AutoRefresh(GaugeChart) @@ -154,15 +156,7 @@ RefreshingGraph.propTypes = { onZoom: func, resizeCoords: shape(), grabDataForDownload: func, - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ), + colors: colorsStringSchema, cellID: string, inView: bool, tableOptions: shape({}), diff --git a/ui/src/shared/components/SingleStat.js b/ui/src/shared/components/SingleStat.js index dc91e872db..99e17e12ef 100644 --- a/ui/src/shared/components/SingleStat.js +++ b/ui/src/shared/components/SingleStat.js @@ -6,6 +6,7 @@ import lastValues from 'shared/parsing/lastValues' import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers' import {DYGRAPH_CONTAINER_V_MARGIN} from 'shared/constants' import {generateThresholdsListHexs} from 'shared/constants/colorOperations' +import {colorsStringSchema} from 'shared/schemas' class SingleStat extends PureComponent { render() { @@ -78,15 +79,7 @@ SingleStat.propTypes = { data: arrayOf(shape()).isRequired, isFetchingInitially: bool, cellHeight: number, - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ), + colors: colorsStringSchema, prefix: string, suffix: string, lineGraph: bool, diff --git a/ui/src/shared/components/StaticLegend.js b/ui/src/shared/components/StaticLegend.js index f79c6cf115..7de93f177a 100644 --- a/ui/src/shared/components/StaticLegend.js +++ b/ui/src/shared/components/StaticLegend.js @@ -86,10 +86,6 @@ class StaticLegend extends Component { key={uuid.v4()} onMouseDown={this.handleClick(i)} > -
{removeMeasurement(v)}
))} diff --git a/ui/src/shared/components/TableGraph.js b/ui/src/shared/components/TableGraph.js index 60edc76fc7..3cb2623a2e 100644 --- a/ui/src/shared/components/TableGraph.js +++ b/ui/src/shared/components/TableGraph.js @@ -27,6 +27,7 @@ import { } from 'src/shared/constants/tableGraph' import {generateThresholdsListHexs} from 'shared/constants/colorOperations' +import {colorsStringSchema} from 'shared/schemas' class TableGraph extends Component { constructor(props) { @@ -432,15 +433,7 @@ TableGraph.propTypes = { }), hoverTime: string, onSetHoverTime: func, - colors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired - ), + colors: colorsStringSchema, setDataLabels: func, } diff --git a/ui/src/shared/components/ThresholdsList.js b/ui/src/shared/components/ThresholdsList.js index 42ba1f9821..669e23fd07 100644 --- a/ui/src/shared/components/ThresholdsList.js +++ b/ui/src/shared/components/ThresholdsList.js @@ -10,6 +10,7 @@ import Threshold from 'src/dashboards/components/Threshold' import ColorDropdown from 'shared/components/ColorDropdown' import {updateThresholdsListColors} from 'src/dashboards/actions/cellEditorOverlay' +import {colorsNumberSchema} from 'shared/schemas' import { THRESHOLD_COLORS, @@ -166,22 +167,14 @@ class ThresholdsList extends Component { ) } } -const {arrayOf, bool, func, number, shape, string} = PropTypes +const {bool, func, string} = PropTypes ThresholdsList.defaultProps = { showListHeading: false, } ThresholdsList.propTypes = { thresholdsListType: string.isRequired, - thresholdsListColors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }).isRequired - ), + thresholdsListColors: colorsNumberSchema.isRequired, handleUpdateThresholdsListColors: func.isRequired, onResetFocus: func.isRequired, showListHeading: bool, diff --git a/ui/src/shared/constants/colorOperations.js b/ui/src/shared/constants/colorOperations.js index 275289703e..3c745c904f 100644 --- a/ui/src/shared/constants/colorOperations.js +++ b/ui/src/shared/constants/colorOperations.js @@ -1,36 +1,21 @@ import _ from 'lodash' +import chroma from 'chroma-js' + import { THRESHOLD_COLORS, THRESHOLD_TYPE_BASE, THRESHOLD_TYPE_TEXT, } from 'shared/constants/thresholds' -const hexToRgb = hex => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) - return result - ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - } - : null -} - -const averageRgbValues = valuesObject => { - const {r, g, b} = valuesObject - return (r + g + b) / 3 -} - -const trueNeutralGrey = 128 +const luminanceThreshold = 0.5 const getLegibleTextColor = bgColorHex => { - const averageBackground = averageRgbValues(hexToRgb(bgColorHex)) - const isBackgroundLight = averageBackground > trueNeutralGrey - const darkText = '#292933' const lightText = '#ffffff' - return isBackgroundLight ? darkText : lightText + return chroma(bgColorHex).luminance() < luminanceThreshold + ? darkText + : lightText } const findNearestCrossedThreshold = (colors, lastValue) => { diff --git a/ui/src/shared/constants/graphColorPalettes.js b/ui/src/shared/constants/graphColorPalettes.js new file mode 100644 index 0000000000..3b3768b4cc --- /dev/null +++ b/ui/src/shared/constants/graphColorPalettes.js @@ -0,0 +1,228 @@ +import chroma from 'chroma-js' + +const COLOR_TYPE_SCALE = 'scale' + +// Color Palettes +export const LINE_COLORS_A = [ + { + type: COLOR_TYPE_SCALE, + hex: '#31C0F6', + id: '0', + name: 'Nineteen Eighty Four', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#A500A5', + id: '0', + name: 'Nineteen Eighty Four', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#FF7E27', + id: '0', + name: 'Nineteen Eighty Four', + value: '0', + }, +] + +export const LINE_COLORS_B = [ + { + type: COLOR_TYPE_SCALE, + hex: '#74D495', + id: '1', + name: 'Atlantis', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#3F3FBA', + id: '1', + name: 'Atlantis', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#FF4D9E', + id: '1', + name: 'Atlantis', + value: '0', + }, +] + +export const LINE_COLORS_C = [ + { + type: COLOR_TYPE_SCALE, + hex: '#8F8AF4', + id: '1', + name: 'Do Androids Dream of Electric Sheep?', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#A51414', + id: '1', + name: 'Do Androids Dream of Electric Sheep?', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#F4CF31', + id: '1', + name: 'Do Androids Dream of Electric Sheep?', + value: '0', + }, +] + +export const LINE_COLORS_D = [ + { + type: COLOR_TYPE_SCALE, + hex: '#FD7A5D', + id: '1', + name: 'Delorean', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#5F1CF2', + id: '1', + name: 'Delorean', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#4CE09A', + id: '1', + name: 'Delorean', + value: '0', + }, +] + +export const LINE_COLORS_E = [ + { + type: COLOR_TYPE_SCALE, + hex: '#FDC44F', + id: '1', + name: 'Cthulhu', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#007C76', + id: '1', + name: 'Cthulhu', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#8983FF', + id: '1', + name: 'Cthulhu', + value: '0', + }, +] + +export const LINE_COLORS_F = [ + { + type: COLOR_TYPE_SCALE, + hex: '#DA6FF1', + id: '1', + name: 'Ectoplasm', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#00717A', + id: '1', + name: 'Ectoplasm', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#ACFF76', + id: '1', + name: 'Ectoplasm', + value: '0', + }, +] + +export const LINE_COLORS_G = [ + { + type: COLOR_TYPE_SCALE, + hex: '#F6F6F8', + id: '1', + name: 'T-Max 400 Film', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#A4A8B6', + id: '1', + name: 'T-Max 400 Film', + value: '0', + }, + { + type: COLOR_TYPE_SCALE, + hex: '#545667', + id: '1', + name: 'T-Max 400 Film', + value: '0', + }, +] + +export const LINE_COLORS_RULE_GRAPH = [ + { + type: COLOR_TYPE_SCALE, + hex: '#4ED8A0', + id: '1', + name: 'Rainforest', + value: '0', + }, +] + +export const DEFAULT_LINE_COLORS = LINE_COLORS_A + +export const LINE_COLOR_SCALES = [ + LINE_COLORS_A, + LINE_COLORS_B, + LINE_COLORS_C, + LINE_COLORS_D, + LINE_COLORS_E, + LINE_COLORS_F, + LINE_COLORS_G, +].map(colorScale => { + const name = colorScale[0].name + const colors = colorScale + const id = colorScale[0].id + + return {name, colors, id} +}) + +export const validateLineColors = colors => { + if (!colors || colors.length === 0) { + return DEFAULT_LINE_COLORS + } + + const testColorsTypes = + colors.filter(color => color.type === COLOR_TYPE_SCALE).length === + colors.length + + return testColorsTypes ? colors : DEFAULT_LINE_COLORS +} + +export const getLineColorsHexes = (colors, numSeries) => { + const validatedColors = validateLineColors(colors) // ensures safe defaults + const colorsHexArray = validatedColors.map(color => color.hex) + + if (numSeries === 1 || numSeries === 0) { + return [colorsHexArray[0]] + } + if (numSeries === 2) { + return [colorsHexArray[0], colorsHexArray[1]] + } + return chroma + .scale(colorsHexArray) + .mode('lch') + .colors(numSeries) +} diff --git a/ui/src/shared/schemas.js b/ui/src/shared/schemas.js index 35df59a72b..5e16535433 100644 --- a/ui/src/shared/schemas.js +++ b/ui/src/shared/schemas.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' -const {shape, string} = PropTypes +const {arrayOf, number, shape, string} = PropTypes export const annotation = shape({ id: string.isRequired, @@ -9,3 +9,23 @@ export const annotation = shape({ text: string.isRequired, type: string.isRequired, }) + +export const colorsStringSchema = arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired +) + +export const colorsNumberSchema = arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: number.isRequired, + }).isRequired +) diff --git a/ui/src/status/fixtures.js b/ui/src/status/fixtures.js index d366801897..3e96f036f0 100644 --- a/ui/src/status/fixtures.js +++ b/ui/src/status/fixtures.js @@ -1,3 +1,5 @@ +import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes' + export const fixtureStatusPageCells = [ { i: 'alerts-bar-graph', @@ -8,6 +10,7 @@ export const fixtureStatusPageCells = [ h: 4, legend: {}, name: 'Alert Events per Day – Last 30 Days', + colors: DEFAULT_LINE_COLORS, queries: [ { query: diff --git a/ui/src/style/components/color-dropdown.scss b/ui/src/style/components/color-dropdown.scss index b892cd0b22..99f6901f7b 100644 --- a/ui/src/style/components/color-dropdown.scss +++ b/ui/src/style/components/color-dropdown.scss @@ -4,6 +4,10 @@ */ $color-dropdown--circle: 14px; +$color-dropdown--bar: 104px; +$color-dropdown--bar-height: 10px; +$color-dropdown--left-padding: 11px; +$color-dropdown--name-padding: 20px; .color-dropdown { width: 140px; @@ -31,11 +35,11 @@ $color-dropdown--circle: 14px; position: absolute; top: 30px; left: 0; - z-index: 2; + z-index: 5; width: 100%; border-radius: 4px; box-shadow: 0 2px 5px 0.6px fade-out($g0-obsidian, 0.7); - @include gradient-h($g0-obsidian,$g2-kevlar); + @include gradient-h($g0-obsidian, $g2-kevlar); } .color-dropdown--item { @include no-user-select(); @@ -43,9 +47,7 @@ $color-dropdown--circle: 14px; height: 28px; position: relative; color: $g11-sidewalk; - transition: - color 0.25s ease, - background-color 0.25s ease; + transition: color 0.25s ease, background-color 0.25s ease; &:hover { background-color: $g4-onyx; @@ -67,6 +69,7 @@ $color-dropdown--circle: 14px; } } .color-dropdown--swatch, +.color-dropdown--swatches, .color-dropdown--name { position: absolute; top: 50%; @@ -76,21 +79,35 @@ $color-dropdown--circle: 14px; width: $color-dropdown--circle; height: $color-dropdown--circle; border-radius: 50%; - left: 11px; + left: $color-dropdown--left-padding; +} +.color-dropdown--swatches { + width: $color-dropdown--bar; + height: $color-dropdown--bar-height; + border-radius: $color-dropdown--bar-height / 2; + left: $color-dropdown--left-padding; } .color-dropdown--name { text-align: left; - right: 11px; - left: 34px; + right: $color-dropdown--name-padding; + left: $color-dropdown--circle + $color-dropdown--name-padding; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 600; text-transform: capitalize; + + .color-dropdown--swatches + & { + left: $color-dropdown--bar + $color-dropdown--name-padding; + } } -.color-dropdown .color-dropdown--menu .fancy-scroll--container .fancy-scroll--track-v .fancy-scroll--thumb-v { - @include gradient-v($g9-mountain,$g7-graphite); +.color-dropdown + .color-dropdown--menu + .fancy-scroll--container + .fancy-scroll--track-v + .fancy-scroll--thumb-v { + @include gradient-v($g9-mountain, $g7-graphite); } .color-dropdown--toggle.color-dropdown__disabled { color: $g7-graphite; diff --git a/ui/src/style/components/static-legend.scss b/ui/src/style/components/static-legend.scss index a9600a11a5..252620299d 100644 --- a/ui/src/style/components/static-legend.scss +++ b/ui/src/style/components/static-legend.scss @@ -16,17 +16,9 @@ flex-wrap: wrap; max-height: 50%; overflow: auto; - @include custom-scrollbar($g3-castle,$g6-smoke); -} -.static-legend--dot { - display: inline-block; - vertical-align: middle; - margin-right: 4px; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: $g20-white; + @include custom-scrollbar($g3-castle, $g6-smoke); } + .static-legend--item, .static-legend--single { height: 22px; @@ -43,8 +35,7 @@ .static-legend--item { transition: background-color 0.25s ease, color 0.25s ease; - span, - .static-legend--dot { + span { opacity: 0.8; transition: opacity 0.25s ease; } @@ -53,8 +44,7 @@ cursor: pointer; background-color: $g6-smoke; - span, - .static-legend--dot { + span { opacity: 1; } } @@ -62,16 +52,14 @@ background-color: $g1-raven; font-style: italic; - span, - .static-legend--dot { + span { opacity: 0.35; } &:hover { background-color: $g2-kevlar; - span, - .static-legend--dot { + span { opacity: 0.65; } } diff --git a/ui/src/style/pages/admin.scss b/ui/src/style/pages/admin.scss index ab3e7d56ab..1731246c4f 100644 --- a/ui/src/style/pages/admin.scss +++ b/ui/src/style/pages/admin.scss @@ -16,9 +16,7 @@ .tab { font-weight: 500 !important; border-radius: $radius $radius 0 0 !important; - transition: - background-color 0.25s ease, - color 0.25s ease !important; + transition: background-color 0.25s ease, color 0.25s ease !important; border: 0 !important; text-align: left; height: 60px !important; @@ -41,16 +39,24 @@ } } .admin-tabs--content { - .panel {border-top-left-radius: 0;} - .panel-heading {height: 60px;} + .panel { + border-top-left-radius: 0; + } + .panel-heading { + height: 60px; + } .panel-title { font-size: 17px; font-weight: 400 !important; color: $g12-forge; padding: 6px 0; } - .panel-body {min-height: 300px;} - .panel-heading + .panel-body {padding-top: 0;} + .panel-body { + min-height: 300px; + } + .panel-heading + .panel-body { + padding-top: 0; + } } /* @@ -69,7 +75,9 @@ border-radius: $radius 0 0 $radius !important; padding: 0 0 0 16px !important; } - & + div {padding-left: 0;} + & + div { + padding-left: 0; + } } } @@ -82,8 +90,13 @@ font-weight: 600; color: $g14-chromium; transition: none !important; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; - .caret {opacity: 0;} + .caret { + opacity: 0; + } } .admin-table--multi-select-empty .dropdown-toggle { color: $g8-storm; @@ -92,7 +105,9 @@ color: $g20-white !important; background-color: $c-pool; - .caret {opacity: 1;} + .caret { + opacity: 1; + } &:hover { transition: background-color 0.25s ease; @@ -100,7 +115,9 @@ } } table > tbody > tr > td.admin-table--left-offset, -table > thead > tr > th.admin-table--left-offset {padding-left: 15px;} +table > thead > tr > th.admin-table--left-offset { + padding-left: 15px; +} table > tbody > tr.admin-table--edit-row, table > tbody > tr.admin-table--edit-row:hover, @@ -141,9 +158,15 @@ pre.admin-table--query { .db-manager { margin-bottom: 8px; - &:last-child {margin-bottom: 0;} - .db-manager-header--actions {visibility: hidden;} - &:hover .db-manager-header--actions {visibility: visible;} + &:last-child { + margin-bottom: 0; + } + .db-manager-header--actions { + visibility: hidden; + } + &:hover .db-manager-header--actions { + visibility: visible; + } } .db-manager-header { padding: 0 11px; @@ -182,7 +205,9 @@ pre.admin-table--query { padding: 9px 11px; border-radius: 0 0 $radius-small $radius-small; - .table-highlight > tbody > tr:hover {background-color: $g5-pepper;} + .table-highlight > tbody > tr:hover { + background-color: $g5-pepper; + } } /* diff --git a/ui/src/types/influxAdmin.ts b/ui/src/types/influxAdmin.ts new file mode 100644 index 0000000000..a9255be652 --- /dev/null +++ b/ui/src/types/influxAdmin.ts @@ -0,0 +1,14 @@ +export interface UserRole { + name: string +} + +interface UserPermission { + name: string +} + +export interface User { + name: string + roles: UserRole[] + permissions: UserPermission[] + password: string +} diff --git a/ui/test/dashboards/reducers/cellEditorOverlay.test.js b/ui/test/dashboards/reducers/cellEditorOverlay.test.js index 2d29ecf67b..eb11c734e0 100644 --- a/ui/test/dashboards/reducers/cellEditorOverlay.test.js +++ b/ui/test/dashboards/reducers/cellEditorOverlay.test.js @@ -8,6 +8,7 @@ import { updateThresholdsListColors, updateThresholdsListType, updateGaugeColors, + updateLineColors, updateAxes, } from 'src/dashboards/actions/cellEditorOverlay' import {DEFAULT_TABLE_OPTIONS} from 'src/shared/constants/tableGraph' @@ -17,6 +18,7 @@ import { validateThresholdsListColors, getThresholdsListType, } from 'shared/constants/thresholds' +import {validateLineColors} from 'src/shared/constants/graphColorPalettes' const defaultCellType = 'line' const defaultCellName = 'defaultCell' @@ -45,6 +47,7 @@ const defaultThresholdsListColors = validateThresholdsListColors( defaultThresholdsListType ) const defaultGaugeColors = validateGaugeColors(defaultCell.colors) +const defaultLineColors = validateLineColors(defaultCell.colors) describe('Dashboards.Reducers.cellEditorOverlay', () => { it('should show cell editor overlay', () => { @@ -117,4 +120,11 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => { expect(actual.cell.axes).toBe(expected) }) + + it('should update the cell line graph colors', () => { + const actual = reducer(initialState, updateLineColors(defaultLineColors)) + const expected = defaultLineColors + + expect(actual.lineColors).toBe(expected) + }) }) diff --git a/ui/yarn.lock b/ui/yarn.lock index 0c053dee44..b8c44de637 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1686,6 +1686,10 @@ chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" +chroma-js@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-1.3.6.tgz#22dd7220ef6b55dcfcb8ef92982baaf55dced45d" + ci-info@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4"