Merge pull request #3559 from influxdata/feature/export-import-dashboards

Export and import dashboards
pull/10616/head
Iris Scholten 2018-06-05 11:02:12 -07:00 committed by GitHub
commit 484c41197e
31 changed files with 1789 additions and 714 deletions

View File

@ -3,6 +3,7 @@
### Features
1. [#3522](https://github.com/influxdata/chronograf/pull/3522): Add support for Template Variables in Cell Titles
1. [#3559](https://github.com/influxdata/chronograf/pull/3559): Add ability to export and import dashboards
### UI Improvements

View File

@ -1,364 +0,0 @@
import {
getDashboards as getDashboardsAJAX,
updateDashboard as updateDashboardAJAX,
deleteDashboard as deleteDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX,
runTemplateVariableQuery,
} from 'src/dashboards/apis'
import {notify} from 'shared/actions/notifications'
import {errorThrown} from 'shared/actions/errors'
import {
getNewDashboardCell,
getClonedDashboardCell,
} from 'src/dashboards/utils/cellGetters'
import {
notifyDashboardDeleted,
notifyDashboardDeleteFailed,
notifyCellAdded,
notifyCellDeleted,
} from 'shared/copy/notifications'
import {
TEMPLATE_VARIABLE_SELECTED,
TEMPLATE_VARIABLES_SELECTED_BY_NAME,
} from 'shared/constants/actionTypes'
import {makeQueryForTemplate} from 'src/dashboards/utils/templateVariableQueryGenerator'
import parsers from 'shared/parsing'
export const loadDashboards = (dashboards, dashboardID) => ({
type: 'LOAD_DASHBOARDS',
payload: {
dashboards,
dashboardID,
},
})
export const loadDeafaultDashTimeV1 = dashboardID => ({
type: 'ADD_DASHBOARD_TIME_V1',
payload: {
dashboardID,
},
})
export const addDashTimeV1 = (dashboardID, timeRange) => ({
type: 'ADD_DASHBOARD_TIME_V1',
payload: {
dashboardID,
timeRange,
},
})
export const setDashTimeV1 = (dashboardID, timeRange) => ({
type: 'SET_DASHBOARD_TIME_V1',
payload: {
dashboardID,
timeRange,
},
})
export const setTimeRange = timeRange => ({
type: 'SET_DASHBOARD_TIME_RANGE',
payload: {
timeRange,
},
})
export const updateDashboard = dashboard => ({
type: 'UPDATE_DASHBOARD',
payload: {
dashboard,
},
})
export const deleteDashboard = dashboard => ({
type: 'DELETE_DASHBOARD',
payload: {
dashboard,
dashboardID: dashboard.id,
},
})
export const deleteDashboardFailed = dashboard => ({
type: 'DELETE_DASHBOARD_FAILED',
payload: {
dashboard,
},
})
export const updateDashboardCells = (dashboard, cells) => ({
type: 'UPDATE_DASHBOARD_CELLS',
payload: {
dashboard,
cells,
},
})
export const syncDashboardCell = (dashboard, cell) => ({
type: 'SYNC_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
export const addDashboardCell = (dashboard, cell) => ({
type: 'ADD_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
export const editDashboardCell = (dashboard, x, y, isEditing) => ({
type: 'EDIT_DASHBOARD_CELL',
// x and y coords are used as a alternative to cell ids, which are not
// universally unique, and cannot be because React depends on a
// quasi-predictable ID for keys. Since cells cannot overlap, coordinates act
// as a suitable id
payload: {
dashboard,
x, // x-coord of the cell to be edited
y, // y-coord of the cell to be edited
isEditing,
},
})
export const cancelEditCell = (dashboardID, cellID) => ({
type: 'CANCEL_EDIT_CELL',
payload: {
dashboardID,
cellID,
},
})
export const renameDashboardCell = (dashboard, x, y, name) => ({
type: 'RENAME_DASHBOARD_CELL',
payload: {
dashboard,
x, // x-coord of the cell to be renamed
y, // y-coord of the cell to be renamed
name,
},
})
export const deleteDashboardCell = (dashboard, cell) => ({
type: 'DELETE_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
export const editCellQueryStatus = (queryID, status) => ({
type: 'EDIT_CELL_QUERY_STATUS',
payload: {
queryID,
status,
},
})
export const templateVariableSelected = (dashboardID, templateID, values) => ({
type: TEMPLATE_VARIABLE_SELECTED,
payload: {
dashboardID,
templateID,
values,
},
})
export const templateVariablesSelectedByName = (dashboardID, query) => ({
type: TEMPLATE_VARIABLES_SELECTED_BY_NAME,
payload: {
dashboardID,
query,
},
})
export const editTemplateVariableValues = (
dashboardID,
templateID,
values
) => ({
type: 'EDIT_TEMPLATE_VARIABLE_VALUES',
payload: {
dashboardID,
templateID,
values,
},
})
export const setHoverTime = hoverTime => ({
type: 'SET_HOVER_TIME',
payload: {
hoverTime,
},
})
export const setActiveCell = activeCellID => ({
type: 'SET_ACTIVE_CELL',
payload: {
activeCellID,
},
})
// Async Action Creators
export const getDashboardsAsync = () => async dispatch => {
try {
const {
data: {dashboards},
} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
return dashboards
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
const removeUnselectedTemplateValues = dashboard => {
const templates = dashboard.templates.map(template => {
if (template.type === 'csv') {
return template
}
const value = template.values.find(val => val.selected)
const values = value ? [value] : []
return {...template, values}
})
return templates
}
export const putDashboard = dashboard => async dispatch => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
dashboard
)
const {
data: dashboardWithOnlySelectedTemplateValues,
} = await updateDashboardAJAX({
...dashboard,
templates: templatesWithOnlySelectedValues,
})
// save all template values to redux
dispatch(
updateDashboard({
...dashboardWithOnlySelectedTemplateValues,
templates: dashboard.templates,
})
)
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const putDashboardByID = dashboardID => async (dispatch, getState) => {
try {
const {
dashboardUI: {dashboards},
} = getState()
const dashboard = dashboards.find(d => d.id === +dashboardID)
const templates = removeUnselectedTemplateValues(dashboard)
await updateDashboardAJAX({...dashboard, templates})
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateDashboardCell = (dashboard, cell) => async dispatch => {
try {
const {data} = await updateDashboardCellAJAX(cell)
dispatch(syncDashboardCell(dashboard, data))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardAsync = dashboard => async dispatch => {
dispatch(deleteDashboard(dashboard))
try {
await deleteDashboardAJAX(dashboard)
dispatch(notify(notifyDashboardDeleted(dashboard.name)))
} catch (error) {
dispatch(
errorThrown(
error,
notifyDashboardDeleteFailed(dashboard.name, error.data.message)
)
)
dispatch(deleteDashboardFailed(dashboard))
}
}
export const addDashboardCellAsync = (
dashboard,
cellType
) => async dispatch => {
try {
const {data} = await addDashboardCellAJAX(
dashboard,
getNewDashboardCell(dashboard, cellType)
)
dispatch(addDashboardCell(dashboard, data))
dispatch(notify(notifyCellAdded(data.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => {
try {
const clonedCell = getClonedDashboardCell(dashboard, cell)
const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
dispatch(addDashboardCell(dashboard, data))
dispatch(notify(notifyCellAdded(clonedCell.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
try {
await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(dashboard, cell))
dispatch(notify(notifyCellDeleted(cell.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateTempVarValues = (source, dashboard) => async dispatch => {
try {
const tempsWithQueries = dashboard.templates.filter(
({query}) => !!query.influxql
)
const asyncQueries = tempsWithQueries.map(({query}) =>
runTemplateVariableQuery(source, {query: makeQueryForTemplate(query)})
)
const results = await Promise.all(asyncQueries)
results.forEach(({data}, i) => {
const {type, query, id} = tempsWithQueries[i]
const parsed = parsers[type](data, query.tagKey || query.measurement)
const vals = parsed[type]
dispatch(editTemplateVariableValues(dashboard.id, id, vals))
})
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}

View File

@ -0,0 +1,688 @@
import _ from 'lodash'
import {
getDashboards as getDashboardsAJAX,
updateDashboard as updateDashboardAJAX,
deleteDashboard as deleteDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX,
runTemplateVariableQuery,
createDashboard as createDashboardAJAX,
} from 'src/dashboards/apis'
import {getMe} from 'src/shared/apis/auth'
import {notify} from 'src/shared/actions/notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {
getNewDashboardCell,
getClonedDashboardCell,
} from 'src/dashboards/utils/cellGetters'
import {
notifyDashboardDeleted,
notifyDashboardDeleteFailed,
notifyCellAdded,
notifyCellDeleted,
notifyDashboardImportFailed,
notifyDashboardImported,
} from 'src/shared/copy/notifications'
import {
TEMPLATE_VARIABLE_SELECTED,
TEMPLATE_VARIABLES_SELECTED_BY_NAME,
} from 'src/shared/constants/actionTypes'
import {CellType} from 'src/types/dashboard'
import {makeQueryForTemplate} from 'src/dashboards/utils/templateVariableQueryGenerator'
import parsers from 'src/shared/parsing'
import {getDeep} from 'src/utils/wrappers'
import {Dashboard, TimeRange, Cell, Query, Source, Template} from 'src/types'
interface LoadDashboardsAction {
type: 'LOAD_DASHBOARDS'
payload: {
dashboards: Dashboard[]
dashboardID: string
}
}
export const loadDashboards = (
dashboards: Dashboard[],
dashboardID?: string
): LoadDashboardsAction => ({
type: 'LOAD_DASHBOARDS',
payload: {
dashboards,
dashboardID,
},
})
interface LoadDeafaultDashTimeV1Action {
type: 'ADD_DASHBOARD_TIME_V1'
payload: {
dashboardID: string
}
}
export const loadDeafaultDashTimeV1 = (
dashboardID: string
): LoadDeafaultDashTimeV1Action => ({
type: 'ADD_DASHBOARD_TIME_V1',
payload: {
dashboardID,
},
})
interface AddDashTimeV1Action {
type: 'ADD_DASHBOARD_TIME_V1'
payload: {
dashboardID: string
timeRange: TimeRange
}
}
export const addDashTimeV1 = (
dashboardID: string,
timeRange: TimeRange
): AddDashTimeV1Action => ({
type: 'ADD_DASHBOARD_TIME_V1',
payload: {
dashboardID,
timeRange,
},
})
interface SetDashTimeV1Action {
type: 'SET_DASHBOARD_TIME_V1'
payload: {
dashboardID: string
timeRange: TimeRange
}
}
export const setDashTimeV1 = (
dashboardID: string,
timeRange: TimeRange
): SetDashTimeV1Action => ({
type: 'SET_DASHBOARD_TIME_V1',
payload: {
dashboardID,
timeRange,
},
})
interface SetTimeRangeAction {
type: 'SET_DASHBOARD_TIME_RANGE'
payload: {
timeRange: TimeRange
}
}
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
type: 'SET_DASHBOARD_TIME_RANGE',
payload: {
timeRange,
},
})
interface UpdateDashboardAction {
type: 'UPDATE_DASHBOARD'
payload: {
dashboard: Dashboard
}
}
export const updateDashboard = (
dashboard: Dashboard
): UpdateDashboardAction => ({
type: 'UPDATE_DASHBOARD',
payload: {
dashboard,
},
})
interface CreateDashboardAction {
type: 'CREATE_DASHBOARD'
payload: {
dashboard: Dashboard
}
}
export const createDashboard = (
dashboard: Dashboard
): CreateDashboardAction => ({
type: 'CREATE_DASHBOARD',
payload: {
dashboard,
},
})
interface DeleteDashboardAction {
type: 'DELETE_DASHBOARD'
payload: {
dashboard: Dashboard
dashboardID: number
}
}
export const deleteDashboard = (
dashboard: Dashboard
): DeleteDashboardAction => ({
type: 'DELETE_DASHBOARD',
payload: {
dashboard,
dashboardID: dashboard.id,
},
})
interface DeleteDashboardFailedAction {
type: 'DELETE_DASHBOARD_FAILED'
payload: {
dashboard: Dashboard
}
}
export const deleteDashboardFailed = (
dashboard: Dashboard
): DeleteDashboardFailedAction => ({
type: 'DELETE_DASHBOARD_FAILED',
payload: {
dashboard,
},
})
interface UpdateDashboardCellsAction {
type: 'UPDATE_DASHBOARD_CELLS'
payload: {
dashboard: Dashboard
cells: Cell[]
}
}
export const updateDashboardCells = (
dashboard: Dashboard,
cells: Cell[]
): UpdateDashboardCellsAction => ({
type: 'UPDATE_DASHBOARD_CELLS',
payload: {
dashboard,
cells,
},
})
interface SyncDashboardCellAction {
type: 'SYNC_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
cell: Cell
}
}
export const syncDashboardCell = (
dashboard: Dashboard,
cell: Cell
): SyncDashboardCellAction => ({
type: 'SYNC_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
interface AddDashboardCellAction {
type: 'ADD_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
cell: Cell
}
}
export const addDashboardCell = (
dashboard: Dashboard,
cell: Cell
): AddDashboardCellAction => ({
type: 'ADD_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
interface EditDashboardCellAction {
type: 'EDIT_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
x: number
y: number
isEditing: boolean
}
}
export const editDashboardCell = (
dashboard: Dashboard,
x: number,
y: number,
isEditing: boolean
): EditDashboardCellAction => ({
type: 'EDIT_DASHBOARD_CELL',
// x and y coords are used as a alternative to cell ids, which are not
// universally unique, and cannot be because React depends on a
// quasi-predictable ID for keys. Since cells cannot overlap, coordinates act
// as a suitable id
payload: {
dashboard,
x, // x-coord of the cell to be edited
y, // y-coord of the cell to be edited
isEditing,
},
})
interface CancelEditCellAction {
type: 'CANCEL_EDIT_CELL'
payload: {
dashboardID: string
cellID: string
}
}
export const cancelEditCell = (
dashboardID: string,
cellID: string
): CancelEditCellAction => ({
type: 'CANCEL_EDIT_CELL',
payload: {
dashboardID,
cellID,
},
})
interface RenameDashboardCellAction {
type: 'RENAME_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
x: number
y: number
name: string
}
}
export const renameDashboardCell = (
dashboard: Dashboard,
x: number,
y: number,
name: string
): RenameDashboardCellAction => ({
type: 'RENAME_DASHBOARD_CELL',
payload: {
dashboard,
x, // x-coord of the cell to be renamed
y, // y-coord of the cell to be renamed
name,
},
})
interface DeleteDashboardCellAction {
type: 'DELETE_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
cell: Cell
}
}
export const deleteDashboardCell = (
dashboard: Dashboard,
cell: Cell
): DeleteDashboardCellAction => ({
type: 'DELETE_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
interface EditCellQueryStatusAction {
type: 'EDIT_CELL_QUERY_STATUS'
payload: {
queryID: string
status: string
}
}
export const editCellQueryStatus = (
queryID: string,
status: string
): EditCellQueryStatusAction => ({
type: 'EDIT_CELL_QUERY_STATUS',
payload: {
queryID,
status,
},
})
interface TemplateVariableSelectedAction {
type: 'TEMPLATE_VARIABLE_SELECTED'
payload: {
dashboardID: string
templateID: string
values: any[]
}
}
export const templateVariableSelected = (
dashboardID: string,
templateID: string,
values
): TemplateVariableSelectedAction => ({
type: TEMPLATE_VARIABLE_SELECTED,
payload: {
dashboardID,
templateID,
values,
},
})
interface TemplateVariablesSelectedByNameAction {
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME'
payload: {
dashboardID: string
query: Query
}
}
export const templateVariablesSelectedByName = (
dashboardID: string,
query: Query
): TemplateVariablesSelectedByNameAction => ({
type: TEMPLATE_VARIABLES_SELECTED_BY_NAME,
payload: {
dashboardID,
query,
},
})
interface EditTemplateVariableValuesAction {
type: 'EDIT_TEMPLATE_VARIABLE_VALUES'
payload: {
dashboardID: number
templateID: string
values: any[]
}
}
export const editTemplateVariableValues = (
dashboardID: number,
templateID: string,
values
): EditTemplateVariableValuesAction => ({
type: 'EDIT_TEMPLATE_VARIABLE_VALUES',
payload: {
dashboardID,
templateID,
values,
},
})
interface SetHoverTimeAction {
type: 'SET_HOVER_TIME'
payload: {
hoverTime: string
}
}
export const setHoverTime = (hoverTime: string): SetHoverTimeAction => ({
type: 'SET_HOVER_TIME',
payload: {
hoverTime,
},
})
interface SetActiveCellAction {
type: 'SET_ACTIVE_CELL'
payload: {
activeCellID: string
}
}
export const setActiveCell = (activeCellID: string): SetActiveCellAction => ({
type: 'SET_ACTIVE_CELL',
payload: {
activeCellID,
},
})
// Async Action Creators
export const getDashboardsAsync = () => async (
dispatch
): Promise<Dashboard[] | void> => {
try {
const {
data: {dashboards},
} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
return dashboards
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const getChronografVersion = () => async (): Promise<string | void> => {
try {
const results = await getMe()
const version = _.get(results, 'headers.x-chronograf-version')
return version
} catch (error) {
console.error(error)
}
}
const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
template => {
if (template.type === 'csv') {
return template
}
const value = template.values.find(val => val.selected)
const values = value ? [value] : []
return {...template, values}
}
)
return templates
}
export const putDashboard = (dashboard: Dashboard) => async (
dispatch
): Promise<void> => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
dashboard
)
const {
data: dashboardWithOnlySelectedTemplateValues,
} = await updateDashboardAJAX({
...dashboard,
templates: templatesWithOnlySelectedValues,
})
// save all template values to redux
dispatch(
updateDashboard({
...dashboardWithOnlySelectedTemplateValues,
templates: dashboard.templates,
})
)
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const putDashboardByID = (dashboardID: string) => async (
dispatch,
getState
): Promise<void> => {
try {
const {
dashboardUI: {dashboards},
} = getState()
const dashboard: Dashboard = dashboards.find(d => d.id === +dashboardID)
const templates = removeUnselectedTemplateValues(dashboard)
await updateDashboardAJAX({...dashboard, templates})
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateDashboardCell = (dashboard: Dashboard, cell: Cell) => async (
dispatch
): Promise<void> => {
try {
const {data} = await updateDashboardCellAJAX(cell)
dispatch(syncDashboardCell(dashboard, data))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardAsync = (dashboard: Dashboard) => async (
dispatch
): Promise<void> => {
dispatch(deleteDashboard(dashboard))
try {
await deleteDashboardAJAX(dashboard)
dispatch(notify(notifyDashboardDeleted(dashboard.name)))
} catch (error) {
dispatch(
errorThrown(
error,
notifyDashboardDeleteFailed(dashboard.name, error.data.message)
)
)
dispatch(deleteDashboardFailed(dashboard))
}
}
export const addDashboardCellAsync = (
dashboard: Dashboard,
cellType: CellType
) => async (dispatch): Promise<void> => {
try {
const {data} = await addDashboardCellAJAX(
dashboard,
getNewDashboardCell(dashboard, cellType)
)
dispatch(addDashboardCell(dashboard, data))
dispatch(notify(notifyCellAdded(data.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const cloneDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch): Promise<void> => {
try {
const clonedCell = getClonedDashboardCell(dashboard, cell)
const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
dispatch(addDashboardCell(dashboard, data))
dispatch(notify(notifyCellAdded(clonedCell.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch): Promise<void> => {
try {
await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(dashboard, cell))
dispatch(notify(notifyCellDeleted(cell.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateTempVarValues = (
source: Source,
dashboard: Dashboard
) => async (dispatch): Promise<void> => {
try {
const tempsWithQueries = dashboard.templates.filter(
({query}) => !!_.get(query, 'influxql')
)
const asyncQueries = tempsWithQueries.map(({query}) =>
runTemplateVariableQuery(source, {
query: makeQueryForTemplate(query),
db: null,
tempVars: null,
})
)
const results = await Promise.all(asyncQueries)
results.forEach(({data}, i) => {
const {type, query, id} = tempsWithQueries[i]
const parsed = parsers[type](data, query.tagKey || query.measurement)
const vals = parsed[type]
dispatch(editTemplateVariableValues(dashboard.id, id, vals))
})
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const importDashboardAsync = (dashboard: Dashboard) => async (
dispatch
): Promise<void> => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
dashboard
)
const results = await createDashboardAJAX({
...dashboard,
templates: templatesWithOnlySelectedValues,
})
const dashboardWithOnlySelectedTemplateValues = _.get(results, 'data')
// save all template values to redux
dispatch(
createDashboard({
...dashboardWithOnlySelectedTemplateValues,
templates: dashboard.templates,
})
)
const {
data: {dashboards},
} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
dispatch(notify(notifyDashboardImported(name)))
} catch (error) {
const errorMessage = _.get(
error,
'data.message',
'Could not upload dashboard'
)
dispatch(notify(notifyDashboardImportFailed('', errorMessage)))
console.error(error)
dispatch(errorThrown(error))
}
}

View File

@ -1,8 +1,8 @@
import React from 'react'
import SourceIndicator from 'shared/components/SourceIndicator'
import SourceIndicator from 'src/shared/components/SourceIndicator'
const DashboardsHeader = () => (
const DashboardsHeader = (): JSX.Element => (
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">

View File

@ -1,98 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
import SearchBar from 'src/hosts/components/SearchBar'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class DashboardsPageContents extends Component {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
}
}
filterDashboards = searchTerm => {
this.setState({searchTerm})
}
render() {
const {
dashboards,
onDeleteDashboard,
onCreateDashboard,
onCloneDashboard,
dashboardLink,
} = this.props
const {searchTerm} = this.state
let tableHeader
if (dashboards === null) {
tableHeader = 'Loading Dashboards...'
} else if (dashboards.length === 1) {
tableHeader = '1 Dashboard'
} else {
tableHeader = `${dashboards.length} Dashboards`
}
const filteredDashboards = dashboards.filter(d =>
d.name.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="panel">
<div className="panel-heading">
<h2 className="panel-title">{tableHeader}</h2>
<div className="dashboards-page--actions">
<SearchBar
placeholder="Filter by Name..."
onSearch={this.filterDashboards}
/>
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
>
<span className="icon plus" /> Create Dashboard
</button>
</Authorized>
</div>
</div>
<div className="panel-body">
<DashboardsTable
dashboards={filteredDashboards}
onDeleteDashboard={onDeleteDashboard}
onCreateDashboard={onCreateDashboard}
onCloneDashboard={onCloneDashboard}
dashboardLink={dashboardLink}
/>
</div>
</div>
</div>
</div>
</div>
</FancyScrollbar>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
DashboardsPageContents.propTypes = {
dashboards: arrayOf(shape()),
onDeleteDashboard: func.isRequired,
onCreateDashboard: func.isRequired,
onCloneDashboard: func.isRequired,
dashboardLink: string.isRequired,
}
export default DashboardsPageContents

View File

@ -0,0 +1,165 @@
import React, {Component, MouseEvent} from 'react'
import {connect} from 'react-redux'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
import ImportDashboardOverlay from 'src/dashboards/components/ImportDashboardOverlay'
import SearchBar from 'src/hosts/components/SearchBar'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
showOverlay as showOverlayAction,
ShowOverlay,
} from 'src/shared/actions/overlayTechnology'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Dashboard} from 'src/types'
import {Notification} from 'src/types/notifications'
interface Props {
dashboards: Dashboard[]
onDeleteDashboard: (dashboard: Dashboard) => () => void
onCreateDashboard: () => void
onCloneDashboard: (
dashboard: Dashboard
) => (event: MouseEvent<HTMLButtonElement>) => void
onExportDashboard: (dashboard: Dashboard) => () => void
onImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
showOverlay: ShowOverlay
dashboardLink: string
}
interface State {
searchTerm: string
}
@ErrorHandling
class DashboardsPageContents extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
}
}
public render() {
const {
onDeleteDashboard,
onCreateDashboard,
onCloneDashboard,
onExportDashboard,
dashboardLink,
} = this.props
return (
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="panel">
{this.renderPanelHeading}
<div className="panel-body">
<DashboardsTable
dashboards={this.filteredDashboards}
onDeleteDashboard={onDeleteDashboard}
onCreateDashboard={onCreateDashboard}
onCloneDashboard={onCloneDashboard}
onExportDashboard={onExportDashboard}
dashboardLink={dashboardLink}
/>
</div>
</div>
</div>
</div>
</div>
</FancyScrollbar>
)
}
private get renderPanelHeading(): JSX.Element {
const {onCreateDashboard} = this.props
return (
<div className="panel-heading">
<h2 className="panel-title">{this.panelTitle}</h2>
<div className="panel-controls">
<SearchBar
placeholder="Filter by Name..."
onSearch={this.filterDashboards}
/>
<Authorized requiredRole={EDITOR_ROLE}>
<>
<button
className="btn btn-sm btn-default"
onClick={this.showImportOverlay}
>
<span className="icon import" /> Import Dashboard
</button>
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
>
<span className="icon plus" /> Create Dashboard
</button>
</>
</Authorized>
</div>
</div>
)
}
private get filteredDashboards(): Dashboard[] {
const {dashboards} = this.props
const {searchTerm} = this.state
return dashboards.filter(d =>
d.name.toLowerCase().includes(searchTerm.toLowerCase())
)
}
private get panelTitle(): string {
const {dashboards} = this.props
if (dashboards === null) {
return 'Loading Dashboards...'
} else if (dashboards.length === 1) {
return '1 Dashboard'
}
return `${dashboards.length} Dashboards`
}
private filterDashboards = (searchTerm: string): void => {
this.setState({searchTerm})
}
private showImportOverlay = (): void => {
const {showOverlay, onImportDashboard, notify} = this.props
const options = {
dismissOnClickOutside: false,
dismissOnEscape: false,
}
showOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<ImportDashboardOverlay
onDismissOverlay={onDismissOverlay}
onImportDashboard={onImportDashboard}
notify={notify}
/>
)}
</OverlayContext.Consumer>,
options
)
}
}
const mapDispatchToProps = {
showOverlay: showOverlayAction,
}
export default connect(null, mapDispatchToProps)(DashboardsPageContents)

View File

@ -1,116 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import ConfirmButton from 'shared/components/ConfirmButton'
const AuthorizedEmptyState = ({onCreateDashboard}) => (
<div className="generic-empty-state">
<h4 style={{marginTop: '90px'}}>
Looks like you dont have any dashboards
</h4>
<br />
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
style={{marginBottom: '90px'}}
>
<span className="icon plus" /> Create Dashboard
</button>
</div>
)
const unauthorizedEmptyState = (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>Looks like you dont have any dashboards</h4>
</div>
)
const DashboardsTable = ({
dashboards,
onDeleteDashboard,
onCreateDashboard,
onCloneDashboard,
dashboardLink,
}) => {
return dashboards && dashboards.length ? (
<table className="table v-center admin-table table-highlight">
<thead>
<tr>
<th>Name</th>
<th>Template Variables</th>
<th />
</tr>
</thead>
<tbody>
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
<tr key={dashboard.id}>
<td>
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
{dashboard.name}
</Link>
</td>
<td>
{dashboard.templates.length ? (
dashboard.templates.map(tv => (
<code className="table--temp-var" key={tv.id}>
{tv.tempVar}
</code>
))
) : (
<span className="empty-string">None</span>
)}
</td>
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={<td />}
>
<td className="text-right">
<button
className="btn btn-xs btn-default table--show-on-row-hover"
onClick={onCloneDashboard(dashboard)}
>
<span className="icon duplicate" />
Clone
</button>
<ConfirmButton
confirmAction={onDeleteDashboard(dashboard)}
size="btn-xs"
type="btn-danger"
text="Delete"
customClass="table--show-on-row-hover"
/>
</td>
</Authorized>
</tr>
))}
</tbody>
</table>
) : (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={unauthorizedEmptyState}
>
<AuthorizedEmptyState onCreateDashboard={onCreateDashboard} />
</Authorized>
)
}
const {arrayOf, func, shape, string} = PropTypes
DashboardsTable.propTypes = {
dashboards: arrayOf(shape()),
onDeleteDashboard: func.isRequired,
onCreateDashboard: func.isRequired,
onCloneDashboard: func.isRequired,
dashboardLink: string.isRequired,
}
AuthorizedEmptyState.propTypes = {
onCreateDashboard: func.isRequired,
}
export default DashboardsTable

View File

@ -0,0 +1,147 @@
import React, {PureComponent, MouseEvent} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE, VIEWER_ROLE} from 'src/auth/Authorized'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {Dashboard, Template} from 'src/types'
interface Props {
dashboards: Dashboard[]
onDeleteDashboard: (dashboard: Dashboard) => () => void
onCreateDashboard: () => void
onCloneDashboard: (
dashboard: Dashboard
) => (event: MouseEvent<HTMLButtonElement>) => void
onExportDashboard: (dashboard: Dashboard) => () => void
dashboardLink: string
}
class DashboardsTable extends PureComponent<Props> {
public render() {
const {
dashboards,
dashboardLink,
onCloneDashboard,
onDeleteDashboard,
onExportDashboard,
} = this.props
if (!dashboards.length) {
return this.emptyStateDashboard
}
return (
<table className="table v-center admin-table table-highlight">
<thead>
<tr>
<th>Name</th>
<th>Template Variables</th>
<th />
</tr>
</thead>
<tbody>
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
<tr key={dashboard.id}>
<td>
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
{dashboard.name}
</Link>
</td>
<td>{this.getDashboardTemplates(dashboard)}</td>
<td className="text-right">
<Authorized
requiredRole={VIEWER_ROLE}
replaceWithIfNotAuthorized={<div />}
>
<button
className="btn btn-xs btn-default table--show-on-row-hover"
onClick={onExportDashboard(dashboard)}
>
<span className="icon export" />Export
</button>
</Authorized>
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={<div />}
>
<>
<button
className="btn btn-xs btn-default table--show-on-row-hover"
onClick={onCloneDashboard(dashboard)}
>
<span className="icon duplicate" />
Clone
</button>
<ConfirmButton
confirmAction={onDeleteDashboard(dashboard)}
size="btn-xs"
type="btn-danger"
text="Delete"
customClass="table--show-on-row-hover"
/>
</>
</Authorized>
</td>
</tr>
))}
</tbody>
</table>
)
}
private getDashboardTemplates = (
dashboard: Dashboard
): JSX.Element | JSX.Element[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', [])
if (templates.length) {
return templates.map(tv => (
<code className="table--temp-var" key={tv.id}>
{tv.tempVar}
</code>
))
}
return <span className="empty-string">None</span>
}
private get emptyStateDashboard(): JSX.Element {
const {onCreateDashboard} = this.props
return (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={this.unauthorizedEmptyState}
>
<div className="generic-empty-state">
<h4 style={{marginTop: '90px'}}>
Looks like you dont have any dashboards
</h4>
<br />
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
style={{marginBottom: '90px'}}
>
<span className="icon plus" /> Create Dashboard
</button>
</div>
</Authorized>
)
}
private get unauthorizedEmptyState(): JSX.Element {
return (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>
Looks like you dont have any dashboards
</h4>
</div>
)
}
}
export default DashboardsTable

View File

@ -0,0 +1,84 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import Container from 'src/shared/components/overlay/OverlayContainer'
import Heading from 'src/shared/components/overlay/OverlayHeading'
import Body from 'src/shared/components/overlay/OverlayBody'
import DragAndDrop from 'src/shared/components/DragAndDrop'
import {notifyDashboardImportFailed} from 'src/shared/copy/notifications'
import {Dashboard} from 'src/types'
import {Notification} from 'src/types/notifications'
interface Props {
onDismissOverlay: () => void
onImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
}
interface State {
isImportable: boolean
}
class ImportDashboardOverlay extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isImportable: false,
}
}
public render() {
const {onDismissOverlay} = this.props
return (
<Container maxWidth={800}>
<Heading title="Import Dashboard" onDismiss={onDismissOverlay} />
<Body>
<DragAndDrop
submitText="Upload Dashboard"
fileTypesToAccept={this.validFileExtension}
handleSubmit={this.handleUploadDashboard}
/>
</Body>
</Container>
)
}
private get validFileExtension(): string {
return '.json'
}
private handleUploadDashboard = (
uploadContent: string,
fileName: string
): void => {
const {onImportDashboard, onDismissOverlay} = this.props
const fileExtensionRegex = new RegExp(`${this.validFileExtension}$`)
if (!fileName.match(fileExtensionRegex)) {
this.props.notify(
notifyDashboardImportFailed(fileName, 'Please import a JSON file')
)
return
}
try {
const {dashboard} = JSON.parse(uploadContent)
if (!_.isEmpty(dashboard)) {
onImportDashboard(dashboard)
} else {
this.props.notify(
notifyDashboardImportFailed(fileName, 'No dashboard found in file')
)
}
} catch (error) {
this.props.notify(notifyDashboardImportFailed(fileName, error))
}
onDismissOverlay()
}
}
export default ImportDashboardOverlay

View File

@ -100,8 +100,9 @@ type NewDefaultDashboard = Pick<
cells: NewDefaultCell[]
}
>
export const DEFAULT_DASHBOARD_NAME = 'Name This Dashboard'
export const NEW_DASHBOARD: NewDefaultDashboard = {
name: 'Name This Dashboard',
name: DEFAULT_DASHBOARD_NAME,
cells: [NEW_DEFAULT_DASHBOARD_CELL],
}
@ -141,7 +142,15 @@ export const TEMPLATE_VARIABLE_TYPES = {
tagValues: 'tagValue',
}
export const TEMPLATE_VARIABLE_QUERIES = {
interface TemplateVariableQueries {
databases: string
measurements: string
fieldKeys: string
tagKeys: string
tagValues: string
}
export const TEMPLATE_VARIABLE_QUERIES: TemplateVariableQueries = {
databases: 'SHOW DATABASES',
measurements: 'SHOW MEASUREMENTS ON :database:',
fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:',

View File

@ -1,98 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {withRouter} from 'react-router'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import DashboardsHeader from 'src/dashboards/components/DashboardsHeader'
import DashboardsContents from 'src/dashboards/components/DashboardsPageContents'
import {createDashboard} from 'src/dashboards/apis'
import {getDashboardsAsync, deleteDashboardAsync} from 'src/dashboards/actions'
import {NEW_DASHBOARD} from 'src/dashboards/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class DashboardsPage extends Component {
componentDidMount() {
this.props.handleGetDashboards()
}
handleCreateDashboard = async () => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard(NEW_DASHBOARD)
push(`/sources/${id}/dashboards/${data.id}`)
}
handleCloneDashboard = dashboard => async () => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard({
...dashboard,
name: `${dashboard.name} (clone)`,
})
push(`/sources/${id}/dashboards/${data.id}`)
}
handleDeleteDashboard = dashboard => () => {
this.props.handleDeleteDashboard(dashboard)
}
render() {
const {dashboards} = this.props
const dashboardLink = `/sources/${this.props.source.id}`
return (
<div className="page">
<DashboardsHeader sourceName={this.props.source.name} />
<DashboardsContents
dashboardLink={dashboardLink}
dashboards={dashboards}
onDeleteDashboard={this.handleDeleteDashboard}
onCreateDashboard={this.handleCreateDashboard}
onCloneDashboard={this.handleCloneDashboard}
/>
</div>
)
}
}
const {arrayOf, func, string, shape} = PropTypes
DashboardsPage.propTypes = {
source: shape({
id: string.isRequired,
name: string.isRequired,
type: string,
links: shape({
proxy: string.isRequired,
}).isRequired,
telegraf: string.isRequired,
}),
router: shape({
push: func.isRequired,
}).isRequired,
handleGetDashboards: func.isRequired,
handleDeleteDashboard: func.isRequired,
dashboards: arrayOf(shape()),
}
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
dashboards,
dashboard,
})
const mapDispatchToProps = dispatch => ({
handleGetDashboards: bindActionCreators(getDashboardsAsync, dispatch),
handleDeleteDashboard: bindActionCreators(deleteDashboardAsync, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(
withRouter(DashboardsPage)
)

View File

@ -0,0 +1,146 @@
import React, {PureComponent} from 'react'
import {withRouter, InjectedRouter} from 'react-router'
import {connect} from 'react-redux'
import download from 'src/external/download'
import _ from 'lodash'
import DashboardsHeader from 'src/dashboards/components/DashboardsHeader'
import DashboardsContents from 'src/dashboards/components/DashboardsPageContents'
import {createDashboard} from 'src/dashboards/apis'
import {
getDashboardsAsync,
deleteDashboardAsync,
getChronografVersion,
importDashboardAsync,
} from 'src/dashboards/actions'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {NEW_DASHBOARD, DEFAULT_DASHBOARD_NAME} from 'src/dashboards/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
notifyDashboardExported,
notifyDashboardExportFailed,
} from 'src/shared/copy/notifications'
import {Source, Dashboard} from 'src/types'
import {Notification} from 'src/types/notifications'
import {DashboardFile} from 'src/types/dashboard'
interface Props {
source: Source
router: InjectedRouter
handleGetDashboards: () => void
handleGetChronografVersion: () => string
handleDeleteDashboard: (dashboard: Dashboard) => void
handleImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
dashboards: Dashboard[]
}
@ErrorHandling
class DashboardsPage extends PureComponent<Props> {
public componentDidMount() {
this.props.handleGetDashboards()
}
public render() {
const {dashboards, notify} = this.props
const dashboardLink = `/sources/${this.props.source.id}`
return (
<div className="page">
<DashboardsHeader />
<DashboardsContents
dashboardLink={dashboardLink}
dashboards={dashboards}
onDeleteDashboard={this.handleDeleteDashboard}
onCreateDashboard={this.handleCreateDashboard}
onCloneDashboard={this.handleCloneDashboard}
onExportDashboard={this.handleExportDashboard}
onImportDashboard={this.handleImportDashboard}
notify={notify}
/>
</div>
)
}
private handleCreateDashboard = async (): Promise<void> => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard(NEW_DASHBOARD)
push(`/sources/${id}/dashboards/${data.id}`)
}
private handleCloneDashboard = (dashboard: Dashboard) => async (): Promise<
void
> => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard({
...dashboard,
name: `${dashboard.name} (clone)`,
})
push(`/sources/${id}/dashboards/${data.id}`)
}
private handleDeleteDashboard = (dashboard: Dashboard) => (): void => {
this.props.handleDeleteDashboard(dashboard)
}
private handleExportDashboard = (dashboard: Dashboard) => async (): Promise<
void
> => {
const dashboardForDownload = await this.modifyDashboardForDownload(
dashboard
)
try {
download(
JSON.stringify(dashboardForDownload, null, '\t'),
`${dashboard.name}.json`,
'text/plain'
)
this.props.notify(notifyDashboardExported(dashboard.name))
} catch (error) {
this.props.notify(notifyDashboardExportFailed(dashboard.name, error))
}
}
private modifyDashboardForDownload = async (
dashboard: Dashboard
): Promise<DashboardFile> => {
const version = await this.props.handleGetChronografVersion()
return {meta: {chronografVersion: version}, dashboard}
}
private handleImportDashboard = async (
dashboard: Dashboard
): Promise<void> => {
const name = _.get(dashboard, 'name', DEFAULT_DASHBOARD_NAME)
await this.props.handleImportDashboard({
...dashboard,
name,
})
}
}
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
dashboards,
dashboard,
})
const mapDispatchToProps = {
handleGetDashboards: getDashboardsAsync,
handleDeleteDashboard: deleteDashboardAsync,
handleGetChronografVersion: getChronografVersion,
handleImportDashboard: importDashboardAsync,
notify: notifyAction,
}
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(DashboardsPage)
)

View File

@ -46,6 +46,14 @@ export default function ui(state = initialState, action) {
return {...state, ...newState}
}
case 'CREATE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {
dashboards: [...state.dashboards, dashboard],
}
return {...state, ...newState}
}
case 'DELETE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {

View File

@ -1,4 +1,10 @@
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
import {Template, TemplateQuery} from 'src/types/dashboard'
interface PartialTemplateWithQuery {
query: string
tempVars: Array<Partial<Template>>
}
const generateTemplateVariableQuery = ({
type,
@ -8,7 +14,7 @@ const generateTemplateVariableQuery = ({
measurement,
tagKey,
},
}) => {
}: Partial<Template>): PartialTemplateWithQuery => {
const tempVars = []
if (database) {
@ -45,7 +51,7 @@ const generateTemplateVariableQuery = ({
})
}
const query = TEMPLATE_VARIABLE_QUERIES[type]
const query: string = TEMPLATE_VARIABLE_QUERIES[type]
return {
query,
@ -53,7 +59,12 @@ const generateTemplateVariableQuery = ({
}
}
export const makeQueryForTemplate = ({influxql, db, measurement, tagKey}) =>
export const makeQueryForTemplate = ({
influxql,
db,
measurement,
tagKey,
}: TemplateQuery): string =>
influxql
.replace(':database:', `"${db}"`)
.replace(':measurement:', `"${measurement}"`)

View File

@ -0,0 +1,216 @@
import React, {PureComponent, ReactElement, DragEvent} from 'react'
import classnames from 'classnames'
// import {notifyDashboardUploadFailed} from 'src/shared/copy/notifications'
interface Props {
fileTypesToAccept?: string
containerClass?: string
handleSubmit: (uploadContent: string, fileName: string) => void
submitText?: string
}
interface State {
inputContent: string | null
uploadContent: string
fileName: string
progress: string
dragClass: string
}
let dragCounter = 0
class DragAndDrop extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
submitText: 'Write this File',
}
private fileInput: HTMLInputElement
constructor(props: Props) {
super(props)
this.state = {
inputContent: null,
uploadContent: '',
fileName: '',
progress: '',
dragClass: 'drag-none',
}
}
public render() {
return (
<div className={this.containerClass}>
{/* (Invisible, covers entire screen)
This div handles drag only*/}
<div
onDrop={this.handleFile(true)}
onDragOver={this.handleDragOver}
onDragEnter={this.handleDragEnter}
onDragExit={this.handleDragLeave}
onDragLeave={this.handleDragLeave}
className="drag-and-drop--dropzone"
/>
{/* visible form, handles drag & click */}
{this.dragArea}
</div>
)
}
private get dragArea(): ReactElement<HTMLDivElement> {
return (
<div
className={this.dragAreaClass}
onClick={this.handleFileOpen}
onDrop={this.handleFile(true)}
onDragOver={this.handleDragOver}
onDragEnter={this.handleDragEnter}
onDragExit={this.handleDragLeave}
onDragLeave={this.handleDragLeave}
>
{this.dragAreaHeader}
<div className={this.infoClass} />
<input
type="file"
ref={r => (this.fileInput = r)}
className="drag-and-drop--input"
accept={this.fileTypesToAccept}
onChange={this.handleFile(false)}
/>
{this.buttons}
</div>
)
}
private get fileTypesToAccept(): string {
const {fileTypesToAccept} = this.props
if (!fileTypesToAccept) {
return '*'
}
return fileTypesToAccept
}
private get containerClass(): string {
const {dragClass} = this.state
return `drag-and-drop ${dragClass}`
}
private get infoClass(): string {
const {uploadContent} = this.state
return classnames('drag-and-drop--graphic', {success: uploadContent})
}
private get dragAreaClass(): string {
const {uploadContent} = this.state
return classnames('drag-and-drop--form', {active: !uploadContent})
}
private get dragAreaHeader(): ReactElement<HTMLHeadElement> {
const {uploadContent, fileName} = this.state
if (uploadContent) {
return <div className="drag-and-drop--header selected">{fileName}</div>
}
return (
<div className="drag-and-drop--header empty">
Drop a file here or click to upload
</div>
)
}
private get buttons(): ReactElement<HTMLSpanElement> | null {
const {uploadContent} = this.state
const {submitText} = this.props
if (!uploadContent) {
return null
}
return (
<span className="drag-and-drop--buttons">
<button className="btn btn-sm btn-success" onClick={this.handleSubmit}>
{submitText}
</button>
<button
className="btn btn-sm btn-default"
onClick={this.handleCancelFile}
>
Cancel
</button>
</span>
)
}
private handleSubmit = () => {
const {handleSubmit} = this.props
const {uploadContent, fileName} = this.state
handleSubmit(uploadContent, fileName)
}
private handleFile = (drop: boolean) => (e: any): void => {
let file
if (drop) {
file = e.dataTransfer.files[0]
this.setState({
dragClass: 'drag-none',
})
} else {
file = e.currentTarget.files[0]
}
if (!file) {
return
}
e.preventDefault()
e.stopPropagation()
const reader = new FileReader()
reader.readAsText(file)
reader.onload = loadEvent => {
this.setState({
uploadContent: loadEvent.target.result,
fileName: file.name,
})
}
}
private handleFileOpen = (): void => {
const {uploadContent} = this.state
if (uploadContent === '') {
this.fileInput.click()
}
}
private handleCancelFile = (): void => {
this.setState({uploadContent: ''})
this.fileInput.value = ''
}
private handleDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
}
private handleDragEnter = (e: DragEvent<HTMLDivElement>): void => {
dragCounter += 1
e.preventDefault()
this.setState({dragClass: 'drag-over'})
}
private handleDragLeave = (e: DragEvent<HTMLDivElement>): void => {
dragCounter -= 1
e.preventDefault()
if (dragCounter === 0) {
this.setState({dragClass: 'drag-none'})
}
}
}
export default DragAndDrop

View File

@ -1,12 +1,11 @@
import React, {PureComponent, ComponentClass} from 'react'
import React, {PureComponent, Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dismissOverlay} from 'src/shared/actions/overlayTechnology'
interface Props {
OverlayNode?: ComponentClass<any>
OverlayNode?: Component<any>
dismissOnClickOutside?: boolean
dismissOnEscape?: boolean
transitionTime?: number
@ -98,8 +97,8 @@ const mapStateToProps = ({
transitionTime,
})
const mapDispatchToProps = dispatch => ({
handleDismissOverlay: bindActionCreators(dismissOverlay, dispatch),
})
const mapDispatchToProps = {
handleDismissOverlay: dismissOverlay,
}
export default connect(mapStateToProps, mapDispatchToProps)(Overlay)

View File

@ -0,0 +1,11 @@
import React, {SFC, ReactNode} from 'react'
interface Props {
children: ReactNode
}
const OverlayBody: SFC<Props> = ({children}) => (
<div className="overlay--body">{children}</div>
)
export default OverlayBody

View File

@ -0,0 +1,30 @@
import React, {Component, ReactNode, CSSProperties} from 'react'
interface Props {
children: ReactNode
maxWidth?: number
}
class OverlayContainer extends Component<Props> {
public static defaultProps: Partial<Props> = {
maxWidth: 600,
}
public render() {
const {children} = this.props
return (
<div className="overlay--container" style={this.style}>
{children}
</div>
)
}
private get style(): CSSProperties {
const {maxWidth} = this.props
return {maxWidth: `${maxWidth}px`}
}
}
export default OverlayContainer

View File

@ -0,0 +1,28 @@
import React, {PureComponent, ReactChildren} from 'react'
interface Props {
children?: ReactChildren
title: string
onDismiss?: () => void
}
class OverlayHeading extends PureComponent<Props> {
constructor(props: Props) {
super(props)
}
public render() {
const {title, onDismiss, children} = this.props
return (
<div className="overlay--heading">
<div className="overlay--title">{title}</div>
{onDismiss && (
<button className="overlay--dismiss" onClick={onDismiss} />
)}
{children && children}
</div>
)
}
}
export default OverlayHeading

View File

@ -414,6 +414,30 @@ export const notifyDashboardDeleted = name => ({
message: `Dashboard ${name} deleted successfully.`,
})
export const notifyDashboardExported = name => ({
...defaultSuccessNotification,
icon: 'dash-h',
message: `Dashboard ${name} exported successfully.`,
})
export const notifyDashboardExportFailed = (name, errorMessage) => ({
...defaultErrorNotification,
duration: INFINITE,
message: `Failed to export Dashboard ${name}: ${errorMessage}.`,
})
export const notifyDashboardImported = name => ({
...defaultSuccessNotification,
icon: 'dash-h',
message: `Dashboard ${name} imported successfully.`,
})
export const notifyDashboardImportFailed = (fileName, errorMessage) => ({
...defaultErrorNotification,
duration: INFINITE,
message: `Failed to import Dashboard from file ${fileName}: ${errorMessage}.`,
})
export const notifyDashboardDeleteFailed = (name, errorMessage) =>
`Failed to delete Dashboard ${name}: ${errorMessage}.`

View File

@ -41,6 +41,7 @@
@import 'components/color-dropdown';
@import 'components/custom-time-range';
@import 'components/customize-fields';
@import 'components/drag-and-drop';
@import 'components/dygraphs';
@import 'components/fancy-scrollbars';
@import 'components/fancy-table';

View File

@ -0,0 +1,84 @@
/*
Drag and Drop Styles
------------------------------------------------------------------------------
*/
.drag-and-drop--dropzone {
position: absolute;
top: 50%;
left: 50%;
width: 1000%;
height: 1000%;
transform: translate(-50%, -50%);
z-index: $drag-and-drop--z-dropzone;
}
.drag-and-drop--form {
position: relative;
z-index: $drag-and-drop--z-form;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
background-color: $g2-kevlar;
border: 2px solid $g4-onyx;
border-radius: 3px;
padding: 30px 18px;
transition: background-color 0.25s ease, border-color 0.25s ease;
}
input[type='file'].drag-and-drop--input {
display: none;
}
.drag-and-drop--graphic {
background-image: url(assets/images/drag-drop-icon.svg);
background-size: 100% 100%;
background-position: center center;
width: 90px;
height: 90px;
&.success {
background-image: url(assets/images/drag-drop-icon--success.svg);
}
}
.drag-and-drop--header {
@include no-user-select();
width: 100%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0 30px 0;
font-size: 20px;
font-weight: 400;
&.empty {
color: $g12-forge;
}
&.selected {
color: $c-rainforest;
}
}
.drag-and-drop--buttons {
display: flex;
align-items: center;
flex-wrap: nowrap;
margin-top: 18px;
> button.btn {margin: 0 4px;}
}
/*
Styles for hover state and drag-over state look the same
------------------------------------------------------------------------------
*/
.drag-and-drop--form.active:hover,
.drag-and-drop.drag-over .drag-and-drop--form {
cursor: pointer;
background-color: $g4-onyx;
border-color: $g6-smoke;
}

View File

@ -6,14 +6,6 @@
.search-widget {
position: relative;
&:first-child {
margin-right: 8px;
}
&:only-child {
margin-right: 0;
}
input.form-control {
position: relative;
z-index: 1;

View File

@ -79,3 +79,10 @@ $form-md-font: 15px;
$form-lg-height: 46px;
$form-lg-padding: 17px;
$form-lg-font: 17px;
// Drag & Drop
$drag-and-drop--z-dropzone: 9000;
$drag-and-drop--z-form: $drag-and-drop--z-dropzone + 10;
// Overlays
$overlay-dismiss--z: $drag-and-drop--z-dropzone + 10;

View File

@ -59,16 +59,6 @@ $dash-graph-options-arrow: 8px;
}
}
/*
Dashboard Index Page
------------------------------------------------------
*/
.dashboards-page--actions {
display: flex;
align-items: center;
}
/*
Default Dashboard Mode
------------------------------------------------------

View File

@ -4,7 +4,7 @@
*/
$logs-viewer-graph-height: 240px;
$logs-viewer-search-height: 46px;
$logs-viewer-search-height: 46px;
$logs-viewer-filter-height: 42px;
$logs-viewer-gutter: 60px;
@ -37,7 +37,10 @@ $logs-viewer-gutter: 60px;
.logs-viewer--table-container {
padding: 12px $logs-viewer-gutter 30px $logs-viewer-gutter;
height: calc(100% - #{$logs-viewer-graph-height + $logs-viewer-search-height + $logs-viewer-filter-height});
height: calc(
100% - #{$logs-viewer-graph-height + $logs-viewer-search-height +
$logs-viewer-filter-height}
);
background-color: $g3-castle;
}
@ -122,7 +125,6 @@ $logs-viewer-gutter: 60px;
background-color: $g6-smoke;
color: $g15-platinum;
}
}
.logs-viewer--filter-remove {
@ -153,7 +155,7 @@ $logs-viewer-gutter: 60px;
&:after {
transform: translate(-50%, -50%) rotate(45deg);
}
&:hover {
cursor: pointer;

View File

@ -0,0 +1,76 @@
/*
Overlays
-----------------------------------------------------------------------------
*/
$overlay-title-height: $chronograf-page-header-height;
$overlay-gutter: 30px;
$overlay-min-height: 150px;
.overlay--container {
margin: 0 auto;
}
.overlay--heading {
height: $overlay-title-height;
background: $g0-obsidian;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 $overlay-gutter;
@include no-user-select();
}
.overlay--title {
font-size: 19px;
font-weight: 400;
color: $g17-whisper;
white-space: nowrap;
}
.overlay--dismiss {
position: relative;
z-index: $overlay-dismiss--z;
width: ($overlay-title-height - 20px);
height: ($overlay-title-height - 20px);
position: relative;
background-color: transparent;
border: 0;
outline: none;
/* Use psuedo elements to render the X */
&:before,
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 2px;
border-radius: 1px;
background-color: $g11-sidewalk;
transition: background-color 0.25s ease;
}
&:before {
transform: translate(-50%,-50%) rotate(45deg);
}
&:after {
transform: translate(-50%,-50%) rotate(-45deg);
}
/* Hover State */
&:hover {
cursor: pointer;
}
&:hover:before,
&:hover:after {
background-color: $g18-cloud;
}
}
.overlay--body {
padding: $overlay-gutter;
padding-top: 18px;
border-radius: 0 0 $radius $radius;
min-height: $overlay-min-height;
@include gradient-v($g2-kevlar, $g0-obsidian);
}

View File

@ -29,6 +29,25 @@ $panel-gutter: 30px;
@extend %no-user-select;
}
.panel-controls {
display: flex;
align-items: center;
&:nth-child(1) {
justify-content: flex-start;
> * {
margin-right: 8px;
}
}
&:nth-child(2) {
justify-content: flex-end;
> * {
margin-left: 8px;
}
}
}
.panel-body {
background-color: $g3-castle;
padding: $panel-gutter;

View File

@ -18,3 +18,4 @@
@import 'radio-buttons';
@import 'misc';
@import 'code-styles';
@import 'overlays';

View File

@ -102,13 +102,15 @@ export interface TemplateValue {
selected: boolean
}
interface TemplateQuery {
export interface TemplateQuery {
command: string
db?: string
db: string
database?: string
rp?: string
measurement: string
tagKey: string
fieldKey: string
influxql: string
}
export interface Template {
@ -134,3 +136,12 @@ export interface Dashboard {
organization: string
links?: DashboardLinks
}
interface DashboardFileMetaSection {
chronografVersion?: string
}
export interface DashboardFile {
meta?: DashboardFileMetaSection
dashboard: Dashboard
}

View File

@ -1,7 +1,7 @@
import {LayoutCell, LayoutQuery} from './layouts'
import {Service, NewService} from './services'
import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
import {Template, Cell, CellQuery, Legend, Axes, Dashboard} from './dashboard'
import {
GroupBy,
Query,
@ -58,6 +58,7 @@ export {
Notification,
NotificationFunc,
Axes,
Dashboard,
Service,
NewService,
LayoutCell,