Merge pull request #3559 from influxdata/feature/export-import-dashboards
Export and import dashboardspull/10616/head
commit
484c41197e
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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">
|
|
@ -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
|
|
@ -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)
|
|
@ -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 don’t 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 don’t 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
|
|
@ -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 don’t 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 don’t have any dashboards
|
||||
</h4>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardsTable
|
|
@ -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
|
|
@ -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:',
|
||||
|
|
|
@ -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)
|
||||
)
|
|
@ -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)
|
||||
)
|
|
@ -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 = {
|
||||
|
|
|
@ -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}"`)
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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}.`
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -59,16 +59,6 @@ $dash-graph-options-arrow: 8px;
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dashboard Index Page
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
.dashboards-page--actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/*
|
||||
Default Dashboard Mode
|
||||
------------------------------------------------------
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
@import 'radio-buttons';
|
||||
@import 'misc';
|
||||
@import 'code-styles';
|
||||
@import 'overlays';
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue