Merge pull request #2174 from influxdata/multitenancy_ui_role_authorization
Implement Role-based authorization UI for Viewer and Editor rolespull/10616/head
commit
17561bd1d6
|
@ -0,0 +1,89 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
|
||||
export const VIEWER_ROLE = 'viewer'
|
||||
export const EDITOR_ROLE = 'editor'
|
||||
export const ADMIN_ROLE = 'admin'
|
||||
export const SUPERADMIN_ROLE = 'superadmin'
|
||||
|
||||
export const isUserAuthorized = (meRole, requiredRole) => {
|
||||
switch (requiredRole) {
|
||||
case VIEWER_ROLE:
|
||||
return (
|
||||
meRole === VIEWER_ROLE ||
|
||||
meRole === EDITOR_ROLE ||
|
||||
meRole === ADMIN_ROLE ||
|
||||
meRole === SUPERADMIN_ROLE
|
||||
)
|
||||
case EDITOR_ROLE:
|
||||
return (
|
||||
meRole === EDITOR_ROLE ||
|
||||
meRole === ADMIN_ROLE ||
|
||||
meRole === SUPERADMIN_ROLE
|
||||
)
|
||||
case ADMIN_ROLE:
|
||||
return meRole === ADMIN_ROLE || meRole === SUPERADMIN_ROLE
|
||||
case SUPERADMIN_ROLE:
|
||||
return meRole === SUPERADMIN_ROLE
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const getMeRole = me => {
|
||||
return _.get(_.first(_.get(me, 'roles', [])), 'name', 'none') // TODO: TBD if 'none' should be returned if none
|
||||
}
|
||||
|
||||
const Authorized = ({
|
||||
children,
|
||||
me,
|
||||
isUsingAuth,
|
||||
requiredRole,
|
||||
replaceWith,
|
||||
propsOverride,
|
||||
}) => {
|
||||
// if me response has not been received yet, render nothing
|
||||
if (typeof isUsingAuth !== 'boolean') {
|
||||
return null
|
||||
}
|
||||
|
||||
// React.isValidElement guards against multiple children wrapped by Authorized
|
||||
const firstChild = React.isValidElement(children) ? children : children[0]
|
||||
|
||||
const meRole = getMeRole(me)
|
||||
|
||||
if (!isUsingAuth || isUserAuthorized(meRole, requiredRole)) {
|
||||
return firstChild
|
||||
}
|
||||
|
||||
if (propsOverride) {
|
||||
return React.cloneElement(firstChild, {...propsOverride})
|
||||
}
|
||||
|
||||
return replaceWith || null
|
||||
}
|
||||
|
||||
const {arrayOf, bool, node, shape, string} = PropTypes
|
||||
|
||||
Authorized.propTypes = {
|
||||
isUsingAuth: bool,
|
||||
replaceWith: node,
|
||||
children: node.isRequired,
|
||||
me: shape({
|
||||
roles: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
}),
|
||||
requiredRole: string.isRequired,
|
||||
propsOverride: shape(),
|
||||
}
|
||||
|
||||
const mapStateToProps = ({auth: {me, isUsingAuth}}) => ({
|
||||
me,
|
||||
isUsingAuth,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps)(Authorized)
|
|
@ -1,6 +1,8 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
|
@ -46,13 +48,22 @@ const DashboardHeader = ({
|
|||
/>
|
||||
: null}
|
||||
{dashboard
|
||||
? <DashboardHeaderEdit
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
activeDashboard={activeDashboard}
|
||||
onEditDashboard={onEditDashboard}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
? <Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWith={
|
||||
<h1 className="page-header__title">
|
||||
{activeDashboard}
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
<DashboardHeaderEdit
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
activeDashboard={activeDashboard}
|
||||
onEditDashboard={onEditDashboard}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
</Authorized>
|
||||
: <h1 className="page-header__title">
|
||||
{activeDashboard}
|
||||
</h1>}
|
||||
|
@ -61,10 +72,15 @@ const DashboardHeader = ({
|
|||
<GraphTips />
|
||||
<SourceIndicator />
|
||||
{dashboard
|
||||
? <button className="btn btn-primary btn-sm" onClick={onAddCell}>
|
||||
<span className="icon plus" />
|
||||
Add Cell
|
||||
</button>
|
||||
? <Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={onAddCell}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
Add Cell
|
||||
</button>
|
||||
</Authorized>
|
||||
: null}
|
||||
{dashboard
|
||||
? <div
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
|
@ -28,12 +30,14 @@ const DashboardsPageContents = ({
|
|||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</Authorized>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<DashboardsTable
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, {PropTypes} from 'react'
|
|||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||
|
||||
const DashboardsTable = ({
|
||||
|
@ -36,11 +38,13 @@ const DashboardsTable = ({
|
|||
)
|
||||
: <span className="empty-string">None</span>}
|
||||
</td>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDeleteDashboard}
|
||||
item={dashboard}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE} replaceWith={<td />}>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDeleteDashboard}
|
||||
item={dashboard}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
</Authorized>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, {PropTypes} from 'react'
|
|||
import classnames from 'classnames'
|
||||
import calculateSize from 'calculate-size'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
const minTempVarDropdownWidth = 146
|
||||
|
@ -75,13 +77,15 @@ const TemplateControlBar = ({
|
|||
This dashboard does not have any Template Variables
|
||||
</div>}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-sm template-control--manage"
|
||||
onClick={onOpenTemplateManager}
|
||||
>
|
||||
<span className="icon cog-thick" />
|
||||
Manage
|
||||
</button>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
className="btn btn-primary btn-sm template-control--manage"
|
||||
onClick={onOpenTemplateManager}
|
||||
>
|
||||
<span className="icon cog-thick" />
|
||||
Manage
|
||||
</button>
|
||||
</Authorized>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ class Dropdown extends Component {
|
|||
>
|
||||
{item.text}
|
||||
</a>
|
||||
{actions.length > 0
|
||||
{actions && actions.length
|
||||
? <div className="dropdown-actions">
|
||||
{actions.map(action => {
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import LayoutCellMenu from 'shared/components/LayoutCellMenu'
|
||||
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
@ -52,17 +54,19 @@ class LayoutCell extends Component {
|
|||
|
||||
return (
|
||||
<div className="dash-graph">
|
||||
<LayoutCellMenu
|
||||
cell={cell}
|
||||
dataExists={!!celldata.length}
|
||||
isDeleting={isDeleting}
|
||||
isEditable={isEditable}
|
||||
onDelete={this.handleDeleteCell}
|
||||
onEdit={this.handleSummonOverlay}
|
||||
handleClickOutside={this.closeMenu}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onCSVDownload={this.handleCSVDownload}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<LayoutCellMenu
|
||||
cell={cell}
|
||||
dataExists={!!celldata.length}
|
||||
isDeleting={isDeleting}
|
||||
isEditable={isEditable}
|
||||
onDelete={this.handleDeleteCell}
|
||||
onEdit={this.handleSummonOverlay}
|
||||
handleClickOutside={this.closeMenu}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onCSVDownload={this.handleCSVDownload}
|
||||
/>
|
||||
</Authorized>
|
||||
<LayoutCellHeader
|
||||
queries={queries}
|
||||
cellName={cell.name}
|
||||
|
@ -72,12 +76,14 @@ class LayoutCell extends Component {
|
|||
{queries.length
|
||||
? children
|
||||
: <div className="graph-empty">
|
||||
<button
|
||||
className="no-query--button btn btn-md btn-primary"
|
||||
onClick={this.handleSummonOverlay(cell)}
|
||||
>
|
||||
<span className="icon plus" /> Add Graph
|
||||
</button>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
className="no-query--button btn btn-md btn-primary"
|
||||
onClick={this.handleSummonOverlay(cell)}
|
||||
>
|
||||
<span className="icon plus" /> Add Graph
|
||||
</button>
|
||||
</Authorized>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,8 @@ import Resizeable from 'react-component-resizable'
|
|||
|
||||
import _ from 'lodash'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Layout from 'src/shared/components/Layout'
|
||||
|
||||
import {
|
||||
|
@ -87,43 +89,59 @@ class LayoutRenderer extends Component {
|
|||
|
||||
return (
|
||||
<Resizeable onResize={this.handleCellResize}>
|
||||
<GridLayout
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.handleCellResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--name'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
draggableHandle: null,
|
||||
}}
|
||||
>
|
||||
{cells.map(cell =>
|
||||
<div key={cell.i}>
|
||||
<Layout
|
||||
key={cell.i}
|
||||
cell={cell}
|
||||
host={host}
|
||||
source={source}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
resizeCoords={resizeCoords}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
onDeleteCell={onDeleteCell}
|
||||
synchronizer={synchronizer}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</GridLayout>
|
||||
<GridLayout
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.handleCellResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--name'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
>
|
||||
{cells.map(cell =>
|
||||
<div key={cell.i}>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isEditable: false,
|
||||
}}
|
||||
>
|
||||
<Layout
|
||||
key={cell.i}
|
||||
cell={cell}
|
||||
host={host}
|
||||
source={source}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
resizeCoords={resizeCoords}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
onDeleteCell={onDeleteCell}
|
||||
synchronizer={synchronizer}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
</Authorized>
|
||||
</div>
|
||||
)}
|
||||
</GridLayout>
|
||||
</Authorized>
|
||||
</Resizeable>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import uuid from 'node-uuid'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
import {getMeRole} from 'src/auth/Authorized'
|
||||
|
||||
const RoleIndicator = ({me, isUsingAuth}) => {
|
||||
if (!isUsingAuth) {
|
||||
return null
|
||||
}
|
||||
|
||||
const roleName = getMeRole(me)
|
||||
|
||||
const RoleTooltip = `<h1>Role: <code>${roleName}</code></h1>`
|
||||
const uuidTooltip = uuid.v4()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="role-indicator"
|
||||
data-for={uuidTooltip}
|
||||
data-tip={RoleTooltip}
|
||||
>
|
||||
<span className="icon user" />
|
||||
<ReactTooltip
|
||||
id={uuidTooltip}
|
||||
effect="solid"
|
||||
html={true}
|
||||
place="bottom"
|
||||
class="influx-tooltip"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, bool, shape, string} = PropTypes
|
||||
|
||||
RoleIndicator.propTypes = {
|
||||
isUsingAuth: bool.isRequired,
|
||||
me: shape({
|
||||
roles: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
}),
|
||||
}
|
||||
|
||||
const mapStateToProps = ({auth: {me, isUsingAuth}}) => ({
|
||||
me,
|
||||
isUsingAuth,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps)(RoleIndicator)
|
|
@ -30,7 +30,7 @@ const authReducer = (state = initialState, action) => {
|
|||
}
|
||||
case 'LOGOUT_LINK_RECEIVED': {
|
||||
const {logoutLink} = action.payload
|
||||
return {...state, logoutLink}
|
||||
return {...state, logoutLink, isUsingAuth: !!logoutLink}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,9 +66,8 @@ const NavBlock = React.createClass({
|
|||
|
||||
render() {
|
||||
const {location, className} = this.props
|
||||
|
||||
const isActive = React.Children.toArray(this.props.children).find(child => {
|
||||
return location.startsWith(child.props.link)
|
||||
return location.startsWith(child.props.link) // if location is undefined, this will fail silently
|
||||
})
|
||||
|
||||
const children = React.Children.map(this.props.children, child => {
|
||||
|
@ -114,19 +113,11 @@ const NavBlock = React.createClass({
|
|||
const NavBar = React.createClass({
|
||||
propTypes: {
|
||||
children: node,
|
||||
location: string.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
const children = React.Children.map(this.props.children, child => {
|
||||
if (child && child.type === NavBlock) {
|
||||
return React.cloneElement(child, {
|
||||
location: this.props.location,
|
||||
})
|
||||
}
|
||||
const {children} = this.props
|
||||
|
||||
return child
|
||||
})
|
||||
return (
|
||||
<nav className="sidebar">
|
||||
{children}
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, {PropTypes} from 'react'
|
|||
import {withRouter, Link} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import Authorized, {ADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import {
|
||||
NavBar,
|
||||
NavBlock,
|
||||
|
@ -22,6 +24,7 @@ const SideNav = React.createClass({
|
|||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
isHidden: bool.isRequired,
|
||||
isUsingAuth: bool,
|
||||
logoutLink: string,
|
||||
customLinks: arrayOf(
|
||||
shape({
|
||||
|
@ -61,13 +64,13 @@ const SideNav = React.createClass({
|
|||
params: {sourceID},
|
||||
location: {pathname: location},
|
||||
isHidden,
|
||||
isUsingAuth,
|
||||
logoutLink,
|
||||
customLinks,
|
||||
} = this.props
|
||||
|
||||
const sourcePrefix = `/sources/${sourceID}`
|
||||
const dataExplorerLink = `${sourcePrefix}/chronograf/data-explorer`
|
||||
const isUsingAuth = !!logoutLink
|
||||
|
||||
const isDefaultPage = location.split('/').includes(DEFAULT_HOME_PAGE)
|
||||
|
||||
|
@ -84,13 +87,25 @@ const SideNav = React.createClass({
|
|||
<span className="sidebar--icon icon cubo-uniform" />
|
||||
</Link>
|
||||
</div>
|
||||
<NavBlock icon="cubo-node" link={`${sourcePrefix}/hosts`}>
|
||||
<NavBlock
|
||||
icon="cubo-node"
|
||||
link={`${sourcePrefix}/hosts`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
||||
</NavBlock>
|
||||
<NavBlock icon="graphline" link={dataExplorerLink}>
|
||||
<NavBlock
|
||||
icon="graphline"
|
||||
link={dataExplorerLink}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={dataExplorerLink} title="Data Explorer" />
|
||||
</NavBlock>
|
||||
<NavBlock icon="dash-h" link={`${sourcePrefix}/dashboards`}>
|
||||
<NavBlock
|
||||
icon="dash-h"
|
||||
link={`${sourcePrefix}/dashboards`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader
|
||||
link={`${sourcePrefix}/dashboards`}
|
||||
title={'Dashboards'}
|
||||
|
@ -100,6 +115,7 @@ const SideNav = React.createClass({
|
|||
matcher="alerts"
|
||||
icon="alert-triangle"
|
||||
link={`${sourcePrefix}/alerts`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/alerts`} title="Alerting" />
|
||||
<NavListItem link={`${sourcePrefix}/alerts`}>History</NavListItem>
|
||||
|
@ -107,17 +123,27 @@ const SideNav = React.createClass({
|
|||
Create
|
||||
</NavListItem>
|
||||
</NavBlock>
|
||||
<NavBlock icon="crown2" link={`${sourcePrefix}/admin`}>
|
||||
<NavHeader link={`${sourcePrefix}/admin`} title="Admin" />
|
||||
</NavBlock>
|
||||
<NavBlock icon="cog-thick" link={`${sourcePrefix}/manage-sources`}>
|
||||
<Authorized requiredRole={ADMIN_ROLE}>
|
||||
<NavBlock
|
||||
icon="crown2"
|
||||
link={`${sourcePrefix}/admin`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/admin`} title="Admin" />
|
||||
</NavBlock>
|
||||
</Authorized>
|
||||
<NavBlock
|
||||
icon="cog-thick"
|
||||
link={`${sourcePrefix}/manage-sources`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader
|
||||
link={`${sourcePrefix}/manage-sources`}
|
||||
title="Configuration"
|
||||
/>
|
||||
</NavBlock>
|
||||
{isUsingAuth
|
||||
? <NavBlock icon="user">
|
||||
? <NavBlock icon="user" location={location}>
|
||||
{customLinks
|
||||
? this.renderUserMenuBlockWithCustomLinks(
|
||||
customLinks,
|
||||
|
@ -135,11 +161,12 @@ const SideNav = React.createClass({
|
|||
})
|
||||
|
||||
const mapStateToProps = ({
|
||||
auth: {logoutLink},
|
||||
auth: {isUsingAuth, logoutLink},
|
||||
app: {ephemeral: {inPresentationMode}},
|
||||
links: {external: {custom: customLinks}},
|
||||
}) => ({
|
||||
isHidden: inPresentationMode,
|
||||
isUsingAuth,
|
||||
logoutLink,
|
||||
customLinks,
|
||||
})
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Link, withRouter} from 'react-router'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
|
||||
|
@ -13,12 +15,14 @@ const kapacitorDropdown = (
|
|||
) => {
|
||||
if (!kapacitors || kapacitors.length === 0) {
|
||||
return (
|
||||
<Link
|
||||
to={`/sources/${source.id}/kapacitors/new`}
|
||||
className="btn btn-xs btn-default"
|
||||
>
|
||||
<span className="icon plus" /> Add Config
|
||||
</Link>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<Link
|
||||
to={`/sources/${source.id}/kapacitors/new`}
|
||||
className="btn btn-xs btn-default"
|
||||
>
|
||||
<span className="icon plus" /> Add Config
|
||||
</Link>
|
||||
</Authorized>
|
||||
)
|
||||
}
|
||||
const kapacitorItems = kapacitors.map(k => {
|
||||
|
@ -39,35 +43,40 @@ const kapacitorDropdown = (
|
|||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="dropdown-260"
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
items={kapacitorItems}
|
||||
onChoose={setActiveKapacitor}
|
||||
addNew={{
|
||||
url: `/sources/${source.id}/kapacitors/new`,
|
||||
text: 'Add Kapacitor',
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
icon: 'pencil',
|
||||
text: 'edit',
|
||||
handler: item => {
|
||||
router.push(`${item.resource}/edit`)
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{addNew: null, actions: null}}
|
||||
>
|
||||
<Dropdown
|
||||
className="dropdown-260"
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
items={kapacitorItems}
|
||||
onChoose={setActiveKapacitor}
|
||||
addNew={{
|
||||
url: `/sources/${source.id}/kapacitors/new`,
|
||||
text: 'Add Kapacitor',
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
icon: 'pencil',
|
||||
text: 'edit',
|
||||
handler: item => {
|
||||
router.push(`${item.resource}/edit`)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'trash',
|
||||
text: 'delete',
|
||||
handler: item => {
|
||||
handleDeleteKapacitor(item.kapacitor)
|
||||
{
|
||||
icon: 'trash',
|
||||
text: 'delete',
|
||||
handler: item => {
|
||||
handleDeleteKapacitor(item.kapacitor)
|
||||
},
|
||||
confirmable: true,
|
||||
},
|
||||
confirmable: true,
|
||||
},
|
||||
]}
|
||||
selected={selected}
|
||||
/>
|
||||
]}
|
||||
selected={selected}
|
||||
/>
|
||||
</Authorized>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -85,12 +94,14 @@ const InfluxTable = ({
|
|||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">InfluxDB Sources</h2>
|
||||
<Link
|
||||
to={`/sources/${source.id}/manage-sources/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
<span className="icon plus" /> Add Source
|
||||
</Link>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<Link
|
||||
to={`/sources/${source.id}/manage-sources/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
<span className="icon plus" /> Add Source
|
||||
</Link>
|
||||
</Authorized>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center margin-bottom-zero table-highlight">
|
||||
|
@ -131,28 +142,41 @@ const InfluxTable = ({
|
|||
</td>
|
||||
<td>
|
||||
<h5 className="margin-zero">
|
||||
<Link
|
||||
to={`${location.pathname}/${s.id}/edit`}
|
||||
className={s.id === source.id ? 'link-success' : null}
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWith={
|
||||
<strong>
|
||||
{s.name}
|
||||
</strong>
|
||||
}
|
||||
>
|
||||
<strong>
|
||||
{s.name}
|
||||
</strong>
|
||||
{s.default ? ' (Default)' : null}
|
||||
</Link>
|
||||
<Link
|
||||
to={`${location.pathname}/${s.id}/edit`}
|
||||
className={
|
||||
s.id === source.id ? 'link-success' : null
|
||||
}
|
||||
>
|
||||
<strong>
|
||||
{s.name}
|
||||
</strong>
|
||||
{s.default ? ' (Default)' : null}
|
||||
</Link>
|
||||
</Authorized>
|
||||
</h5>
|
||||
<span>
|
||||
{s.url}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<a
|
||||
className="btn btn-xs btn-danger table--show-on-row-hover"
|
||||
href="#"
|
||||
onClick={handleDeleteSource(s)}
|
||||
>
|
||||
Delete Source
|
||||
</a>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<a
|
||||
className="btn btn-xs btn-danger table--show-on-row-hover"
|
||||
href="#"
|
||||
onClick={handleDeleteSource(s)}
|
||||
>
|
||||
Delete Source
|
||||
</a>
|
||||
</Authorized>
|
||||
</td>
|
||||
<td className="source-table--kapacitor">
|
||||
{kapacitorDropdown(
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
@import 'components/redacted-input';
|
||||
@import 'components/resizer';
|
||||
@import 'components/search-widget';
|
||||
@import 'components/source-indicator';
|
||||
@import 'components/info-indicators';
|
||||
@import 'components/source-selector';
|
||||
@import 'components/tables';
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
Source Indicator component styles
|
||||
----------------------------------------------------------------
|
||||
Source & Role Indicator component styles
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
.role-indicator,
|
||||
.source-indicator {
|
||||
@include no-user-select();
|
||||
display: inline-block;
|
|
@ -46,6 +46,10 @@ $tooltip-code-color: $c-potassium;
|
|||
line-height: 1.125em;
|
||||
letter-spacing: 0;
|
||||
font-family: $default-font;
|
||||
|
||||
&:only-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
|
@ -163,8 +163,6 @@ $dash-graph-options-arrow: 8px;
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dash-graph--name {
|
||||
position: relative;
|
||||
height: $dash-graph-heading;
|
||||
line-height: $dash-graph-heading;
|
||||
|
@ -172,6 +170,10 @@ $dash-graph-options-arrow: 8px;
|
|||
padding-left: 10px;
|
||||
transition: color 0.25s ease, background-color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
|
||||
&:only-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.dash-graph--name.dash-graph--name__default {
|
||||
font-style: italic;
|
||||
|
|
Loading…
Reference in New Issue