Add initial source management UI
parent
71b2eec624
commit
81c0e53c4a
|
@ -251,7 +251,9 @@ func (h *SourceHandler) handlePostSource(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
if err := encodeResponse(ctx, w, http.StatusCreated, req.Source); err != nil {
|
||||
res := newSourceResponse(req.Source)
|
||||
|
||||
if err := encodeResponse(ctx, w, http.StatusCreated, res); err != nil {
|
||||
EncodeError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import {getBasepath} from 'src/utils/basepath'
|
|||
// Components
|
||||
import App from 'src/App'
|
||||
import GetSources from 'src/shared/containers/GetSources'
|
||||
import SetSource from 'src/shared/containers/SetSource'
|
||||
import SetActiveSource from 'src/shared/containers/SetActiveSource'
|
||||
import GetOrganizations from 'src/shared/containers/GetOrganizations'
|
||||
import Setup from 'src/Setup'
|
||||
import Signin from 'src/Signin'
|
||||
|
@ -26,12 +26,12 @@ import OrganizationView from 'src/organizations/containers/OrganizationView'
|
|||
import TaskEditPage from 'src/tasks/containers/TaskEditPage'
|
||||
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
||||
import DataExplorerPage from 'src/dataExplorer/components/DataExplorerPage'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import {UserPage} from 'src/user'
|
||||
import {LogsPage} from 'src/logs'
|
||||
import NotFound from 'src/shared/components/NotFound'
|
||||
import GetLinks from 'src/shared/containers/GetLinks'
|
||||
import GetMe from 'src/shared/containers/GetMe'
|
||||
import SourcesPage from 'src/sources/components/SourcesPage'
|
||||
|
||||
// Actions
|
||||
import {disablePresentationMode} from 'src/shared/actions/app'
|
||||
|
@ -82,7 +82,7 @@ class Root extends PureComponent {
|
|||
<Route component={GetOrganizations}>
|
||||
<Route component={App}>
|
||||
<Route path="/" component={GetSources}>
|
||||
<Route path="/" component={SetSource}>
|
||||
<Route path="/" component={SetActiveSource}>
|
||||
<Route
|
||||
path="dashboards/:dashboardID"
|
||||
component={DashboardPage}
|
||||
|
@ -98,26 +98,14 @@ class Root extends PureComponent {
|
|||
/>
|
||||
<Route path="tasks/new" component={TaskPage} />
|
||||
<Route path="tasks/:id" component={TaskEditPage} />
|
||||
<Route path="sources/new" component={SourcePage} />
|
||||
<Route
|
||||
path="data-explorer"
|
||||
component={DataExplorerPage}
|
||||
/>
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
<Route
|
||||
path="manage-sources"
|
||||
component={ManageSources}
|
||||
/>
|
||||
<Route
|
||||
path="manage-sources/new"
|
||||
component={SourcePage}
|
||||
/>
|
||||
<Route
|
||||
path="manage-sources/:id/edit"
|
||||
component={SourcePage}
|
||||
/>
|
||||
<Route path="user_profile" component={UserPage} />
|
||||
<Route path="logs" component={LogsPage} />
|
||||
<Route path="sources" component={SourcesPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
createView as createViewAJAX,
|
||||
updateView as updateViewAJAX,
|
||||
} from 'src/dashboards/apis/v2/view'
|
||||
import {getSource} from 'src/sources/apis/v2'
|
||||
import {readSource} from 'src/sources/apis'
|
||||
import {getBuckets} from 'src/shared/apis/v2/buckets'
|
||||
import {executeQueryAsync} from 'src/logs/api/v2'
|
||||
|
||||
|
@ -263,7 +263,7 @@ export const populateBucketsAsync = (
|
|||
export const getSourceAndPopulateBucketsAsync = (sourceURL: string) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
const source = await getSource(sourceURL)
|
||||
const source = await readSource(sourceURL)
|
||||
|
||||
const bucketsLink = getDeep<string | null>(source, 'links.buckets', null)
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import LogsTable from 'src/logs/components/logs_table/LogsTable'
|
|||
|
||||
// Actions
|
||||
import * as logActions from 'src/logs/actions'
|
||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
// Utils
|
||||
|
@ -24,6 +23,7 @@ import {
|
|||
applyChangesToTableData,
|
||||
isEmptyInfiniteData,
|
||||
} from 'src/logs/utils/table'
|
||||
import {getSources} from 'src/sources/selectors'
|
||||
|
||||
// Constants
|
||||
import {NOW, DEFAULT_TAIL_CHUNK_DURATION_MS} from 'src/logs/constants'
|
||||
|
@ -57,7 +57,6 @@ interface TableConfigStateProps {
|
|||
interface DispatchTableConfigProps {
|
||||
notify: typeof notifyAction
|
||||
getConfig: typeof logActions.getLogConfigAsync
|
||||
getSources: typeof getSourcesAsync
|
||||
addFilter: typeof logActions.addFilter // TODO: update addFilters
|
||||
setConfig: typeof logActions.setConfig
|
||||
updateConfig: typeof logActions.updateLogConfigAsync
|
||||
|
@ -120,7 +119,6 @@ class LogsPage extends Component<Props, State> {
|
|||
|
||||
public async componentDidMount() {
|
||||
try {
|
||||
await this.props.getSources()
|
||||
await this.setCurrentSource()
|
||||
await this.props.getConfig(this.configLink)
|
||||
|
||||
|
@ -558,8 +556,8 @@ class LogsPage extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
const mstp = ({
|
||||
sources,
|
||||
const mstp = (state: AppState): StateProps => {
|
||||
const {
|
||||
links,
|
||||
logs: {
|
||||
currentSource,
|
||||
|
@ -572,7 +570,11 @@ const mstp = ({
|
|||
nextTailLowerBound,
|
||||
currentTailUpperBound,
|
||||
},
|
||||
}: AppState): StateProps => ({
|
||||
} = state
|
||||
|
||||
const sources = getSources(state)
|
||||
|
||||
return {
|
||||
links,
|
||||
sources,
|
||||
filters,
|
||||
|
@ -584,11 +586,11 @@ const mstp = ({
|
|||
tableInfiniteData,
|
||||
nextTailLowerBound,
|
||||
currentTailUpperBound,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp: DispatchProps = {
|
||||
notify: notifyAction,
|
||||
getSources: getSourcesAsync,
|
||||
addFilter: logActions.addFilter,
|
||||
updateConfig: logActions.updateLogConfigAsync,
|
||||
createConfig: logActions.createLogConfigAsync,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Libraries
|
||||
import React, {Component} from 'react'
|
||||
import React, {Component, ReactNode} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components
|
||||
|
@ -9,7 +9,7 @@ import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar
|
|||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element[] | JSX.Element
|
||||
children: JSX.Element[] | JSX.Element | ReactNode
|
||||
fullWidth: boolean
|
||||
scrollable: boolean
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class PageContents extends Component<Props> {
|
|||
return classnames('page-contents', {'full-width': fullWidth})
|
||||
}
|
||||
|
||||
private get children(): JSX.Element | JSX.Element[] {
|
||||
private get children(): JSX.Element | JSX.Element[] | ReactNode {
|
||||
const {fullWidth, children} = this.props
|
||||
|
||||
if (fullWidth) {
|
||||
|
|
|
@ -7,8 +7,11 @@ import _ from 'lodash'
|
|||
// Components
|
||||
import NavMenu from 'src/pageLayout/components/NavMenu'
|
||||
|
||||
// Utils
|
||||
import {getSources} from 'src/sources/selectors'
|
||||
|
||||
// Types
|
||||
import {Source} from 'src/types/v2'
|
||||
import {Source, AppState} from 'src/types/v2'
|
||||
import {IconFont} from 'src/clockface'
|
||||
|
||||
// Styles
|
||||
|
@ -62,7 +65,7 @@ class SideNav extends PureComponent<Props> {
|
|||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Status',
|
||||
link: `/${this.sourceParam}`,
|
||||
link: '/',
|
||||
icon: IconFont.Cubouniform,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['status'],
|
||||
|
@ -70,7 +73,7 @@ class SideNav extends PureComponent<Props> {
|
|||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Data Explorer',
|
||||
link: `/data-explorer/${this.sourceParam}`,
|
||||
link: '/data-explorer',
|
||||
icon: IconFont.Capacitor,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['data-explorer'],
|
||||
|
@ -78,7 +81,7 @@ class SideNav extends PureComponent<Props> {
|
|||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Dashboards',
|
||||
link: `/dashboards/${this.sourceParam}`,
|
||||
link: '/dashboards',
|
||||
icon: IconFont.DashJ,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['dashboards'],
|
||||
|
@ -86,7 +89,7 @@ class SideNav extends PureComponent<Props> {
|
|||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Logs',
|
||||
link: `/logs/${this.sourceParam}`,
|
||||
link: '/logs',
|
||||
icon: IconFont.Wood,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['logs'],
|
||||
|
@ -94,7 +97,7 @@ class SideNav extends PureComponent<Props> {
|
|||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Tasks',
|
||||
link: `/tasks/${this.sourceParam}`,
|
||||
link: '/tasks',
|
||||
icon: IconFont.Alerts,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['tasks'],
|
||||
|
@ -102,49 +105,36 @@ class SideNav extends PureComponent<Props> {
|
|||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Organizations',
|
||||
link: `/organizations/${this.sourceParam}`,
|
||||
link: '/organizations',
|
||||
icon: IconFont.Group,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['organizations'],
|
||||
},
|
||||
{
|
||||
type: NavItemType.Icon,
|
||||
title: 'Configuration',
|
||||
link: `/manage-sources/${this.sourceParam}`,
|
||||
title: 'Sources',
|
||||
link: '/sources',
|
||||
icon: IconFont.Wrench,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['manage-sources'],
|
||||
highlightWhen: ['sources'],
|
||||
},
|
||||
{
|
||||
type: NavItemType.Avatar,
|
||||
title: 'My Profile',
|
||||
link: `/user_profile/${this.sourceParam}`,
|
||||
link: '/user_profile',
|
||||
image: LeroyJenkins.avatar,
|
||||
location: location.pathname,
|
||||
highlightWhen: ['user_profile'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private get sourceParam(): string {
|
||||
const {location, sources = []} = this.props
|
||||
|
||||
const {query} = location
|
||||
const defaultSource = sources.find(s => s.default)
|
||||
const id = query.sourceID || _.get(defaultSource, 'id', 0)
|
||||
|
||||
return `?sourceID=${id}`
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
sources,
|
||||
app: {
|
||||
ephemeral: {inPresentationMode},
|
||||
},
|
||||
}) => ({
|
||||
sources,
|
||||
isHidden: inPresentationMode,
|
||||
})
|
||||
const mstp = (state: AppState) => {
|
||||
const isHidden = state.app.ephemeral.inPresentationMode
|
||||
const sources = getSources(state)
|
||||
|
||||
export default connect(mapStateToProps)(withRouter(SideNav))
|
||||
return {sources, isHidden}
|
||||
}
|
||||
|
||||
export default connect(mstp)(withRouter(SideNav))
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
import {deleteSource, getSources as getSourcesAJAX} from 'src/sources/apis/v2'
|
||||
|
||||
import {notify} from './notifications'
|
||||
|
||||
import {HTTP_NOT_FOUND} from 'src/shared/constants'
|
||||
import {serverError} from 'src/shared/copy/notifications'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
export type Action = ActionLoadSources | ActionUpdateSource | ActionAddSource
|
||||
|
||||
// Load Sources
|
||||
export type LoadSources = (sources: Source[]) => ActionLoadSources
|
||||
export interface ActionLoadSources {
|
||||
type: 'LOAD_SOURCES'
|
||||
payload: {
|
||||
sources: Source[]
|
||||
}
|
||||
}
|
||||
|
||||
export const loadSources = (sources: Source[]): ActionLoadSources => ({
|
||||
type: 'LOAD_SOURCES',
|
||||
payload: {
|
||||
sources,
|
||||
},
|
||||
})
|
||||
|
||||
export type UpdateSource = (source: Source) => ActionUpdateSource
|
||||
export interface ActionUpdateSource {
|
||||
type: 'SOURCE_UPDATED'
|
||||
payload: {
|
||||
source: Source
|
||||
}
|
||||
}
|
||||
|
||||
export const updateSource = (source: Source): ActionUpdateSource => ({
|
||||
type: 'SOURCE_UPDATED',
|
||||
payload: {
|
||||
source,
|
||||
},
|
||||
})
|
||||
|
||||
export type AddSource = (source: Source) => ActionAddSource
|
||||
export interface ActionAddSource {
|
||||
type: 'SOURCE_ADDED'
|
||||
payload: {
|
||||
source: Source
|
||||
}
|
||||
}
|
||||
|
||||
export const addSource = (source: Source): ActionAddSource => ({
|
||||
type: 'SOURCE_ADDED',
|
||||
payload: {
|
||||
source,
|
||||
},
|
||||
})
|
||||
|
||||
export type RemoveAndLoadSources = (
|
||||
source: Source
|
||||
) => (dispatch) => Promise<void>
|
||||
// Async action creators
|
||||
export const removeAndLoadSources = (source: Source) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
try {
|
||||
try {
|
||||
await deleteSource(source)
|
||||
} catch (err) {
|
||||
// A 404 means that either a concurrent write occurred or the source
|
||||
// passed to this action creator doesn't exist (or is undefined)
|
||||
if (err.status !== HTTP_NOT_FOUND) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const newSources = await getSourcesAJAX()
|
||||
dispatch(loadSources(newSources))
|
||||
} catch (err) {
|
||||
dispatch(notify(serverError))
|
||||
}
|
||||
}
|
||||
|
||||
export const getSourcesAsync = () => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
const sources = await getSourcesAJAX()
|
||||
dispatch(loadSources(sources))
|
||||
} catch (error) {
|
||||
dispatch(notify(serverError))
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import {Source} from 'src/types/v2'
|
||||
|
||||
export enum ActionTypes {
|
||||
SetSource = 'SET_SOURCE',
|
||||
ResetSource = 'RESET_SOURCE',
|
||||
}
|
||||
|
||||
interface SetSource {
|
||||
type: ActionTypes.SetSource
|
||||
payload: {
|
||||
source: Source
|
||||
}
|
||||
}
|
||||
|
||||
interface ResetSource {
|
||||
type: ActionTypes.ResetSource
|
||||
}
|
||||
|
||||
export type Actions = SetSource | ResetSource
|
||||
|
||||
export const setSource = (source: Source) => ({
|
||||
type: ActionTypes.SetSource,
|
||||
payload: {
|
||||
source,
|
||||
},
|
||||
})
|
||||
|
||||
export const resetSource = () => ({
|
||||
type: ActionTypes.ResetSource,
|
||||
})
|
|
@ -12,6 +12,9 @@ import RefreshingViewSwitcher from 'src/shared/components/RefreshingViewSwitcher
|
|||
// Constants
|
||||
import {emptyGraphCopy} from 'src/shared/copy/cell'
|
||||
|
||||
// Utils
|
||||
import {getActiveSource} from 'src/sources/selectors'
|
||||
|
||||
// Types
|
||||
import {TimeRange} from 'src/types'
|
||||
import {AppState} from 'src/types/v2'
|
||||
|
@ -102,12 +105,10 @@ class RefreshingView extends PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
const mstp = ({source}: AppState): StateProps => {
|
||||
const link = source.links.query
|
||||
const mstp = (state: AppState): StateProps => {
|
||||
const link = getActiveSource(state).links.query
|
||||
|
||||
return {
|
||||
link,
|
||||
}
|
||||
return {link}
|
||||
}
|
||||
|
||||
const mdtp = {}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
|||
interface Props {
|
||||
disabled?: boolean
|
||||
children: JSX.Element[] | JSX.Element
|
||||
customClass?: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -23,9 +24,12 @@ class IndexListRow extends Component<Props> {
|
|||
}
|
||||
|
||||
private get className(): string {
|
||||
const {disabled} = this.props
|
||||
const {disabled, customClass} = this.props
|
||||
|
||||
return classnames('index-list--row', {'index-list--row-disabled': disabled})
|
||||
return classnames('index-list--row', {
|
||||
'index-list--row-disabled': disabled,
|
||||
[customClass]: !!customClass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,12 @@ import MockChild from 'mocks/MockChild'
|
|||
import {source} from 'mocks/dummy'
|
||||
|
||||
jest.mock('src/sources/apis', () => require('mocks/sources/apis'))
|
||||
const getSources = jest.fn(() => Promise.resolve)
|
||||
|
||||
const onReadSources = jest.fn(() => Promise.resolve())
|
||||
|
||||
const setup = (override?) => {
|
||||
const props = {
|
||||
getSources,
|
||||
onReadSources,
|
||||
sources: [source],
|
||||
router: {},
|
||||
location: {
|
||||
|
|
|
@ -4,12 +4,12 @@ import {connect} from 'react-redux'
|
|||
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||
import {readSources} from 'src/sources/actions'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactElement<any>
|
||||
getSources: typeof getSourcesAsync
|
||||
onReadSources: typeof readSources
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -27,7 +27,8 @@ export class GetSources extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
await this.props.getSources()
|
||||
await this.props.onReadSources()
|
||||
|
||||
this.setState({ready: RemoteDataState.Done})
|
||||
}
|
||||
|
||||
|
@ -41,7 +42,7 @@ export class GetSources extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const mdtp = {
|
||||
getSources: getSourcesAsync,
|
||||
onReadSources: readSources,
|
||||
}
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {get} from 'lodash'
|
||||
|
||||
// Actions
|
||||
import {setActiveSource} from 'src/sources/actions'
|
||||
|
||||
// Utils
|
||||
import {getSources, getActiveSource} from 'src/sources/selectors'
|
||||
import {readQueryParams, updateQueryParams} from 'src/shared/utils/queryParams'
|
||||
|
||||
// Types
|
||||
import {Source, AppState} from 'src/types/v2'
|
||||
|
||||
interface PassedInProps {
|
||||
children: React.ReactElement<any>
|
||||
}
|
||||
|
||||
interface ConnectStateProps {
|
||||
activeSourceID: string
|
||||
sources: Source[]
|
||||
source: Source
|
||||
}
|
||||
|
||||
interface ConnectDispatchProps {
|
||||
onSetActiveSource: typeof setActiveSource
|
||||
}
|
||||
|
||||
type Props = ConnectStateProps & ConnectDispatchProps & PassedInProps
|
||||
|
||||
class SetActiveSource extends PureComponent<Props> {
|
||||
public componentDidMount() {
|
||||
this.resolveActiveSource()
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {source} = this.props
|
||||
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.resolveActiveSource()
|
||||
}
|
||||
|
||||
private resolveActiveSource() {
|
||||
const {sources, activeSourceID, onSetActiveSource} = this.props
|
||||
|
||||
const defaultSourceID = get(sources.find(s => s.default), 'id')
|
||||
const querySourceID = readQueryParams().sourceID
|
||||
|
||||
let resolvedSourceID
|
||||
|
||||
if (sources.find(s => s.id === activeSourceID)) {
|
||||
resolvedSourceID = activeSourceID
|
||||
} else if (sources.find(s => s.id === querySourceID)) {
|
||||
resolvedSourceID = querySourceID
|
||||
} else if (defaultSourceID) {
|
||||
resolvedSourceID = defaultSourceID
|
||||
} else if (sources.length) {
|
||||
resolvedSourceID = sources[0]
|
||||
} else {
|
||||
throw new Error('no source exists')
|
||||
}
|
||||
|
||||
if (activeSourceID !== resolvedSourceID) {
|
||||
onSetActiveSource(resolvedSourceID)
|
||||
}
|
||||
|
||||
if (querySourceID !== resolvedSourceID) {
|
||||
updateQueryParams({sourceID: resolvedSourceID})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => ({
|
||||
source: getActiveSource(state),
|
||||
sources: getSources(state),
|
||||
activeSourceID: state.sources.activeSourceID,
|
||||
})
|
||||
|
||||
const mdtp = {
|
||||
onSetActiveSource: setActiveSource,
|
||||
}
|
||||
|
||||
export default connect<ConnectStateProps, ConnectDispatchProps, PassedInProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(SetActiveSource)
|
|
@ -1,130 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {withRouter, InjectedRouter, WithRouterProps} from 'react-router'
|
||||
import {Location} from 'history'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
import {Links} from 'src/types/v2/links'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
import {Notification, NotificationFunc} from 'src/types'
|
||||
|
||||
import {setSource, resetSource} from 'src/shared/actions/v2/source'
|
||||
|
||||
import {getSourceHealth} from 'src/sources/apis/v2'
|
||||
import * as copy from 'src/shared/copy/notifications'
|
||||
|
||||
interface PassedInProps extends WithRouterProps {
|
||||
router: InjectedRouter
|
||||
children: React.ReactElement<any>
|
||||
location: Location
|
||||
}
|
||||
|
||||
interface ConnectStateProps {
|
||||
sources: Source[]
|
||||
links: Links
|
||||
}
|
||||
|
||||
interface ConnectDispatchProps {
|
||||
setSource: typeof setSource
|
||||
resetSource: typeof resetSource
|
||||
notify: (message: Notification | NotificationFunc) => void
|
||||
}
|
||||
|
||||
type Props = ConnectStateProps & ConnectDispatchProps & PassedInProps
|
||||
|
||||
export const SourceContext = React.createContext({})
|
||||
|
||||
class SetSource extends PureComponent<Props> {
|
||||
public render() {
|
||||
return this.props.children && React.cloneElement(this.props.children)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const source = this.source
|
||||
|
||||
if (source) {
|
||||
this.props.setSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidUpdate() {
|
||||
const {router, sources} = this.props
|
||||
const source = this.source
|
||||
const defaultSource = sources.find(s => s.default === true)
|
||||
|
||||
if (this.isRoot) {
|
||||
return router.push(`${this.rootPath}`)
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
if (defaultSource) {
|
||||
return router.push(`${this.path}?sourceID=${defaultSource.id}`)
|
||||
}
|
||||
|
||||
if (sources[0]) {
|
||||
return router.push(`${this.path}?sourceID=${sources[0].id}`)
|
||||
}
|
||||
|
||||
return router.push(`/sources/new?redirectPath=${this.path}`)
|
||||
}
|
||||
|
||||
try {
|
||||
await getSourceHealth(source.links.health)
|
||||
this.props.setSource(source)
|
||||
} catch (error) {
|
||||
this.props.notify(copy.sourceNoLongerAvailable(source.name))
|
||||
this.props.resetSource()
|
||||
}
|
||||
}
|
||||
|
||||
private get path(): string {
|
||||
const {location} = this.props
|
||||
|
||||
if (this.isRoot) {
|
||||
return this.rootPath
|
||||
}
|
||||
|
||||
return `${location.pathname}`
|
||||
}
|
||||
|
||||
private get rootPath(): string {
|
||||
const {links, location} = this.props
|
||||
if (links.defaultDashboard) {
|
||||
const split = links.defaultDashboard.split('/')
|
||||
const id = split[split.length - 1]
|
||||
return `/dashboards/${id}${location.search}`
|
||||
}
|
||||
|
||||
return `/dashboards`
|
||||
}
|
||||
|
||||
private get isRoot(): boolean {
|
||||
const {
|
||||
location: {pathname},
|
||||
} = this.props
|
||||
|
||||
return pathname === '' || pathname === '/'
|
||||
}
|
||||
|
||||
private get source(): Source {
|
||||
const {location, sources} = this.props
|
||||
|
||||
return sources.find(s => s.id === location.query.sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = ({sources, links}) => ({
|
||||
links,
|
||||
sources,
|
||||
})
|
||||
|
||||
const mdtp = {
|
||||
setSource,
|
||||
resetSource,
|
||||
notify,
|
||||
}
|
||||
|
||||
export default connect<ConnectStateProps, ConnectDispatchProps, PassedInProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(withRouter(SetSource))
|
|
@ -137,13 +137,10 @@ export const sourceCreationSucceeded = (sourceName: string): Notification => ({
|
|||
message: `Connected to InfluxDB ${sourceName} successfully.`,
|
||||
})
|
||||
|
||||
export const sourceCreationFailed = (
|
||||
sourceName: string,
|
||||
errorMessage: string
|
||||
): Notification => ({
|
||||
export const sourceCreationFailed = (errorMessage: string): Notification => ({
|
||||
...defaultErrorNotification,
|
||||
icon: 'server2',
|
||||
message: `Unable to connect to InfluxDB ${sourceName}: ${errorMessage}`,
|
||||
message: `Unable to create source: ${errorMessage}`,
|
||||
})
|
||||
|
||||
export const sourceUpdated = (sourceName: string): Notification => ({
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import {Source} from 'src/types/v2'
|
||||
|
||||
import {Actions, ActionTypes} from 'src/shared/actions/v2/source'
|
||||
|
||||
export type SourceState = Source
|
||||
|
||||
const defaultState: SourceState = {
|
||||
name: '',
|
||||
id: '',
|
||||
type: '',
|
||||
url: '',
|
||||
insecureSkipVerify: false,
|
||||
default: false,
|
||||
telegraf: '',
|
||||
links: null,
|
||||
}
|
||||
|
||||
export default (state = defaultState, action: Actions): SourceState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.ResetSource:
|
||||
return {...defaultState}
|
||||
case ActionTypes.SetSource:
|
||||
return {...action.payload.source}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import {browserHistory} from 'react-router'
|
||||
import qs from 'qs'
|
||||
import {pickBy} from 'lodash'
|
||||
|
||||
export const readQueryParams = (): {[key: string]: any} => {
|
||||
return qs.parse(window.location.search, {ignoreQueryPrefix: true})
|
||||
}
|
||||
|
||||
/*
|
||||
Given an object of query parameter keys and values, updates any corresponding
|
||||
query parameters in the URL to match. If the supplied object has a null value
|
||||
for a key, that query parameter will be removed from the URL altogether.
|
||||
*/
|
||||
export const updateQueryParams = (updatedQueryParams: object): void => {
|
||||
const currentQueryString = window.location.search
|
||||
const newQueryParams = pickBy(
|
||||
{
|
||||
...qs.parse(currentQueryString, {ignoreQueryPrefix: true}),
|
||||
...updatedQueryParams,
|
||||
},
|
||||
v => !!v
|
||||
)
|
||||
|
||||
const newQueryString = qs.stringify(newQueryParams)
|
||||
|
||||
browserHistory.replace(`${window.location.pathname}?${newQueryString}`)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// Libraries
|
||||
import {Dispatch} from 'redux'
|
||||
|
||||
// APIs
|
||||
import {
|
||||
readSources as readSourcesAJAX,
|
||||
createSource as createSourceAJAX,
|
||||
updateSource as updateSourceAJAX,
|
||||
deleteSource as deleteSourceAJAX,
|
||||
} from 'src/sources/apis'
|
||||
|
||||
// Types
|
||||
import {Source, GetState} from 'src/types/v2'
|
||||
|
||||
export type Action =
|
||||
| SetActiveSourceAction
|
||||
| SetSourcesAction
|
||||
| SetSourceAction
|
||||
| RemoveSourceAction
|
||||
|
||||
interface SetActiveSourceAction {
|
||||
type: 'SET_ACTIVE_SOURCE'
|
||||
payload: {
|
||||
activeSourceID: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const setActiveSource = (
|
||||
activeSourceID: string | null
|
||||
): SetActiveSourceAction => ({
|
||||
type: 'SET_ACTIVE_SOURCE',
|
||||
payload: {activeSourceID},
|
||||
})
|
||||
|
||||
interface SetSourcesAction {
|
||||
type: 'SET_SOURCES'
|
||||
payload: {
|
||||
sources: Source[]
|
||||
}
|
||||
}
|
||||
|
||||
export const setSources = (sources: Source[]): SetSourcesAction => ({
|
||||
type: 'SET_SOURCES',
|
||||
payload: {sources},
|
||||
})
|
||||
|
||||
interface SetSourceAction {
|
||||
type: 'SET_SOURCE'
|
||||
payload: {
|
||||
source: Source
|
||||
}
|
||||
}
|
||||
|
||||
export const setSource = (source: Source): SetSourceAction => ({
|
||||
type: 'SET_SOURCE',
|
||||
payload: {source},
|
||||
})
|
||||
|
||||
interface RemoveSourceAction {
|
||||
type: 'REMOVE_SOURCE'
|
||||
payload: {
|
||||
sourceID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const removeSource = (sourceID: string): RemoveSourceAction => ({
|
||||
type: 'REMOVE_SOURCE',
|
||||
payload: {sourceID},
|
||||
})
|
||||
|
||||
export const readSources = () => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const sourcesLink = getState().links.sources
|
||||
const sources = await readSourcesAJAX(sourcesLink)
|
||||
|
||||
dispatch(setSources(sources))
|
||||
}
|
||||
|
||||
export const createSource = (attrs: Partial<Source>) => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const sourcesLink = getState().links.sources
|
||||
const source = await createSourceAJAX(sourcesLink, attrs)
|
||||
|
||||
dispatch(setSource(source))
|
||||
}
|
||||
|
||||
export const updateSource = (source: Source) => async (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
const updatedSource = await updateSourceAJAX(source)
|
||||
|
||||
dispatch(setSource(updatedSource))
|
||||
}
|
||||
|
||||
export const deleteSource = (sourceID: string) => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const source = getState().sources.sources[sourceID]
|
||||
|
||||
if (!source) {
|
||||
throw new Error(`no source with ID "${sourceID}" exists`)
|
||||
}
|
||||
|
||||
await deleteSourceAJAX(source)
|
||||
|
||||
dispatch(removeSource(sourceID))
|
||||
}
|
|
@ -1,6 +1,53 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
export const getSourceHealth = async (url: string) => {
|
||||
export const readSources = async (url): Promise<Source[]> => {
|
||||
const {data} = await AJAX({url})
|
||||
|
||||
return data.sources
|
||||
}
|
||||
|
||||
export const readSource = async (url: string): Promise<Source> => {
|
||||
const {data: source} = await AJAX({
|
||||
url,
|
||||
})
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export const createSource = async (
|
||||
url: string,
|
||||
attributes: Partial<Source>
|
||||
): Promise<Source> => {
|
||||
const {data: source} = await AJAX({
|
||||
url,
|
||||
method: 'POST',
|
||||
data: attributes,
|
||||
})
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export const updateSource = async (
|
||||
newSource: Partial<Source>
|
||||
): Promise<Source> => {
|
||||
const {data: source} = await AJAX({
|
||||
url: newSource.links.self,
|
||||
method: 'PATCH',
|
||||
data: newSource,
|
||||
})
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export function deleteSource(source) {
|
||||
return AJAX({
|
||||
url: source.links.self,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export const getSourceHealth = async (url: string): Promise<void> => {
|
||||
try {
|
||||
await AJAX({url})
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
export const getSources = async (): Promise<Source[]> => {
|
||||
try {
|
||||
const {data} = await AJAX({
|
||||
url: '/api/v2/sources',
|
||||
})
|
||||
|
||||
return data.sources
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getSource = async (url: string): Promise<Source> => {
|
||||
try {
|
||||
const {data: source} = await AJAX({
|
||||
url,
|
||||
})
|
||||
|
||||
return source
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createSource = async (
|
||||
url: string,
|
||||
attributes: Partial<Source>
|
||||
): Promise<Source> => {
|
||||
try {
|
||||
const {data: source} = await AJAX({
|
||||
url,
|
||||
method: 'POST',
|
||||
data: attributes,
|
||||
})
|
||||
|
||||
return source
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateSource = async (
|
||||
newSource: Partial<Source>
|
||||
): Promise<Source> => {
|
||||
try {
|
||||
const {data: source} = await AJAX({
|
||||
url: newSource.links.self,
|
||||
method: 'PATCH',
|
||||
data: newSource,
|
||||
})
|
||||
|
||||
return source
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteSource(source) {
|
||||
return AJAX({
|
||||
url: source.links.self,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export const getSourceHealth = async (url: string): Promise<void> => {
|
||||
try {
|
||||
await AJAX({url})
|
||||
} catch (error) {
|
||||
console.error(`Unable to contact source ${url}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import {stripPrefix} from 'src/utils/basepath'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
currentSource: Source
|
||||
}
|
||||
|
||||
class ConnectionLink extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {source} = this.props
|
||||
return (
|
||||
<h5 className="margin-zero">
|
||||
<Link
|
||||
to={`${stripPrefix(location.pathname)}/${source.id}/edit?${
|
||||
this.sourceParam
|
||||
}`}
|
||||
className={this.className}
|
||||
>
|
||||
<strong>{source.name}</strong>
|
||||
{this.default}
|
||||
</Link>
|
||||
</h5>
|
||||
)
|
||||
}
|
||||
|
||||
private get sourceParam(): string {
|
||||
const {currentSource} = this.props
|
||||
|
||||
return `sourceID=${currentSource.id}`
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
if (this.isCurrentSource) {
|
||||
return 'link-success'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private get default(): string {
|
||||
const {source} = this.props
|
||||
if (source.default) {
|
||||
return ' (Default)'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private get isCurrentSource(): boolean {
|
||||
const {source, currentSource} = this.props
|
||||
return source.id === currentSource.id
|
||||
}
|
||||
}
|
||||
|
||||
export default ConnectionLink
|
|
@ -0,0 +1,11 @@
|
|||
.create-source-overlay--heading-buttons {
|
||||
button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-source-overlay {
|
||||
.form--element {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import {
|
||||
OverlayBody,
|
||||
OverlayHeading,
|
||||
OverlayContainer,
|
||||
Button,
|
||||
ComponentColor,
|
||||
ComponentStatus,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
} from 'src/clockface'
|
||||
|
||||
// Actions
|
||||
import {createSource} from 'src/sources/actions'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
|
||||
// Utils
|
||||
import {sourceCreationFailed} from 'src/shared/copy/notifications'
|
||||
|
||||
// Styles
|
||||
import 'src/sources/components/CreateSourceOverlay.scss'
|
||||
|
||||
// Types
|
||||
import {Source} from 'src/types/v2'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
interface DispatchProps {
|
||||
onCreateSource: typeof createSource
|
||||
onNotify: typeof notify
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
type Props = DispatchProps & OwnProps
|
||||
|
||||
interface State {
|
||||
draftSource: Partial<Source>
|
||||
creationStatus: RemoteDataState
|
||||
}
|
||||
|
||||
class CreateSourceOverlay extends PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
draftSource: {
|
||||
name: '',
|
||||
type: 'v1',
|
||||
url: '',
|
||||
},
|
||||
creationStatus: RemoteDataState.NotStarted,
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {onHide} = this.props
|
||||
const {draftSource} = this.state
|
||||
|
||||
return (
|
||||
<div className="create-source-overlay">
|
||||
<OverlayContainer>
|
||||
<OverlayHeading title="Create Source">
|
||||
<div className="create-source-overlay--heading-buttons">
|
||||
<Button text="Cancel" onClick={onHide} />
|
||||
<Button
|
||||
text="Save"
|
||||
color={ComponentColor.Success}
|
||||
status={this.saveButtonStatus}
|
||||
onClick={this.handleSave}
|
||||
/>
|
||||
</div>
|
||||
</OverlayHeading>
|
||||
<OverlayBody>
|
||||
<Form>
|
||||
<Form.Element label="Name">
|
||||
<Input
|
||||
name="name"
|
||||
autoFocus={true}
|
||||
value={draftSource.name}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Form.Element>
|
||||
<Form.Element label="URL">
|
||||
<Input
|
||||
name="url"
|
||||
autoFocus={true}
|
||||
value={draftSource.url}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Form.Element>
|
||||
<Form.Element label="Type">
|
||||
<Radio>
|
||||
<Radio.Button
|
||||
active={draftSource.type === 'v1'}
|
||||
onClick={this.handleChangeType}
|
||||
value="v1"
|
||||
>
|
||||
v1
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
active={draftSource.type === 'v2'}
|
||||
onClick={this.handleChangeType}
|
||||
value="v2"
|
||||
>
|
||||
v2
|
||||
</Radio.Button>
|
||||
</Radio>
|
||||
</Form.Element>
|
||||
</Form>
|
||||
</OverlayBody>
|
||||
</OverlayContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get saveButtonStatus(): ComponentStatus {
|
||||
return ComponentStatus.Default
|
||||
}
|
||||
|
||||
private handleSave = async () => {
|
||||
const {onCreateSource, onNotify, onHide} = this.props
|
||||
const {draftSource} = this.state
|
||||
|
||||
this.setState({creationStatus: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
await onCreateSource(draftSource)
|
||||
onHide()
|
||||
} catch (error) {
|
||||
this.setState({creationStatus: RemoteDataState.Error})
|
||||
onNotify(sourceCreationFailed(error.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const draftSource = {
|
||||
...this.state.draftSource,
|
||||
[e.target.name]: e.target.value,
|
||||
}
|
||||
|
||||
this.setState({draftSource})
|
||||
}
|
||||
|
||||
private handleChangeType = (type: 'v1' | 'v2') => {
|
||||
const draftSource = {
|
||||
...this.state.draftSource,
|
||||
type,
|
||||
}
|
||||
|
||||
this.setState({draftSource})
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onCreateSource: createSource,
|
||||
onNotify: notify,
|
||||
}
|
||||
|
||||
export default connect<{}, DispatchProps, OwnProps>(
|
||||
null,
|
||||
mdtp
|
||||
)(CreateSourceOverlay)
|
|
@ -0,0 +1,53 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Button,
|
||||
ComponentColor,
|
||||
ComponentSize,
|
||||
ComponentStatus,
|
||||
} from 'src/clockface'
|
||||
|
||||
// Types
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
onClick: () => Promise<void>
|
||||
}
|
||||
|
||||
interface State {
|
||||
status: RemoteDataState
|
||||
}
|
||||
|
||||
class DeleteSourceButton extends PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
status: RemoteDataState.NotStarted,
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {status} = this.state
|
||||
|
||||
const buttonStatus =
|
||||
status === RemoteDataState.Loading
|
||||
? ComponentStatus.Loading
|
||||
: ComponentStatus.Default
|
||||
|
||||
return (
|
||||
<Button
|
||||
text="Delete"
|
||||
color={ComponentColor.Danger}
|
||||
size={ComponentSize.ExtraSmall}
|
||||
status={buttonStatus}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = async () => {
|
||||
this.setState({status: RemoteDataState.Loading})
|
||||
this.props.onClick()
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteSourceButton
|
|
@ -1,51 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import InfluxTableHead from 'src/sources/components/InfluxTableHead'
|
||||
import InfluxTableHeader from 'src/sources/components/InfluxTableHeader'
|
||||
import InfluxTableRow from 'src/sources/components/InfluxTableRow'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
sources: Source[]
|
||||
onDeleteSource: (source: Source) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class InfluxTable extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {source, sources, onDeleteSource} = this.props
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel">
|
||||
<InfluxTableHeader source={source} />
|
||||
<div className="panel-body">
|
||||
<table className="table v-center margin-bottom-zero table-highlight">
|
||||
<InfluxTableHead />
|
||||
<tbody>
|
||||
{sources.map(s => {
|
||||
return (
|
||||
<InfluxTableRow
|
||||
key={s.id}
|
||||
source={s}
|
||||
currentSource={source}
|
||||
onDeleteSource={onDeleteSource}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default InfluxTable
|
|
@ -1,15 +0,0 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
const InfluxTableHead: SFC = (): JSX.Element => {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="source-table--connect-col" />
|
||||
<th>InfluxDB Connection</th>
|
||||
<th className="text-right" />
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfluxTableHead
|
|
@ -1,33 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class InfluxTableHeader extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {source} = this.props
|
||||
|
||||
return (
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">
|
||||
<span>Connections</span>
|
||||
</h2>
|
||||
<Link
|
||||
to={`/sources/${source.id}/manage-sources/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
<span className="icon plus" /> Add Connection
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default InfluxTableHeader
|
|
@ -1,79 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
import ConnectionLink from 'src/sources/components/ConnectionLink'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
currentSource: Source
|
||||
onDeleteSource: (source: Source) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class InfluxTableRow extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {source, currentSource} = this.props
|
||||
|
||||
return (
|
||||
<tr className={this.className}>
|
||||
<td>{this.connectButton}</td>
|
||||
<td>
|
||||
<ConnectionLink source={source} currentSource={currentSource} />
|
||||
<span>{source.url}</span>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<ConfirmButton
|
||||
type="btn-danger"
|
||||
size="btn-xs"
|
||||
text="Delete Connection"
|
||||
confirmAction={this.handleDeleteSource}
|
||||
customClass="delete-source table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
private handleDeleteSource = (): void => {
|
||||
this.props.onDeleteSource(this.props.source)
|
||||
}
|
||||
|
||||
private get connectButton(): JSX.Element {
|
||||
const {source} = this.props
|
||||
if (this.isCurrentSource) {
|
||||
return (
|
||||
<div className="btn btn-success btn-xs source-table--connect">
|
||||
Connected
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="btn btn-default btn-xs source-table--connect"
|
||||
to={`/manage-sources?sourceID=${source.id}`}
|
||||
>
|
||||
Connect
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
if (this.isCurrentSource) {
|
||||
return 'highlight'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private get isCurrentSource(): boolean {
|
||||
const {source, currentSource} = this.props
|
||||
return source.id === currentSource.id
|
||||
}
|
||||
}
|
||||
|
||||
export default InfluxTableRow
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
|
||||
import {SourceForm} from 'src/sources/components/SourceForm'
|
||||
|
||||
const setup = (override = {}) => {
|
||||
const noop = () => {}
|
||||
const props = {
|
||||
source: {
|
||||
url: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
telegraf: '',
|
||||
insecureSkipVerify: false,
|
||||
default: false,
|
||||
metaUrl: '',
|
||||
},
|
||||
editMode: false,
|
||||
onSubmit: noop,
|
||||
onInputChange: noop,
|
||||
onBlurSourceURL: noop,
|
||||
isUsingAuth: false,
|
||||
gotoPurgatory: noop,
|
||||
isInitialSource: false,
|
||||
...override,
|
||||
}
|
||||
|
||||
const wrapper = shallow(<SourceForm {...props} />)
|
||||
return {wrapper, props}
|
||||
}
|
||||
|
||||
describe('Sources.Components.SourceForm', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders inputs', () => {
|
||||
const {wrapper} = setup()
|
||||
const inputs = wrapper.find('input')
|
||||
|
||||
expect(inputs.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,180 +0,0 @@
|
|||
import React, {PureComponent, FocusEvent, MouseEvent, ChangeEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
interface Props {
|
||||
source: Partial<Source>
|
||||
editMode: boolean
|
||||
isInitialSource: boolean
|
||||
onSubmit: (e: MouseEvent<HTMLFormElement>) => void
|
||||
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onBlurSourceURL: (e: FocusEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export class SourceForm extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
source,
|
||||
onSubmit,
|
||||
onInputChange,
|
||||
onBlurSourceURL,
|
||||
isInitialSource,
|
||||
} = this.props
|
||||
return (
|
||||
<div className="panel-body">
|
||||
{isInitialSource}
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
<label htmlFor="connect-string">Connection String</label>
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
className="form-control"
|
||||
id="connect-string"
|
||||
placeholder="Address of InfluxDB"
|
||||
onChange={onInputChange}
|
||||
value={source.url}
|
||||
onBlur={onBlurSourceURL}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
className="form-control"
|
||||
id="name"
|
||||
placeholder="Name this source"
|
||||
onChange={onInputChange}
|
||||
value={source.name}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
className="form-control"
|
||||
id="username"
|
||||
onChange={onInputChange}
|
||||
value={source.username}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="form-control"
|
||||
id="password"
|
||||
onChange={onInputChange}
|
||||
value={source.password}
|
||||
/>
|
||||
</div>
|
||||
{this.isEnterprise && (
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="meta-url">Meta Service Connection URL</label>
|
||||
<input
|
||||
type="text"
|
||||
name="metaUrl"
|
||||
className="form-control"
|
||||
id="meta-url"
|
||||
placeholder="http://localhost:8091"
|
||||
onChange={onInputChange}
|
||||
value={source.metaUrl}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
<label htmlFor="telegraf">Telegraf Database</label>
|
||||
<input
|
||||
type="text"
|
||||
name="telegraf"
|
||||
className="form-control"
|
||||
id="telegraf"
|
||||
onChange={onInputChange}
|
||||
value={source.telegraf}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="defaultConnectionCheckbox"
|
||||
name="default"
|
||||
checked={source.default}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<label htmlFor="defaultConnectionCheckbox">
|
||||
Make this the default connection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{this.isHTTPS && (
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="insecureSkipVerifyCheckbox"
|
||||
name="insecureSkipVerify"
|
||||
checked={source.insecureSkipVerify}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<label htmlFor="insecureSkipVerifyCheckbox">Unsafe SSL</label>
|
||||
</div>
|
||||
<label className="form-helper">{insecureSkipVerifyText}</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group form-group-submit text-center col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className={this.submitClass} type="submit">
|
||||
<span className={this.submitIconClass} />
|
||||
{this.submitText}
|
||||
</button>
|
||||
|
||||
<br />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get submitText(): string {
|
||||
const {editMode} = this.props
|
||||
if (editMode) {
|
||||
return 'Save Changes'
|
||||
}
|
||||
|
||||
return 'Add Connection'
|
||||
}
|
||||
|
||||
private get submitIconClass(): string {
|
||||
const {editMode} = this.props
|
||||
return `icon ${editMode ? 'checkmark' : 'plus'}`
|
||||
}
|
||||
|
||||
private get submitClass(): string {
|
||||
const {editMode} = this.props
|
||||
return classnames('btn btn-block', {
|
||||
'btn-primary': editMode,
|
||||
'btn-success': !editMode,
|
||||
})
|
||||
}
|
||||
|
||||
private get isEnterprise(): boolean {
|
||||
const {source} = this.props
|
||||
return _.get(source, 'type', '').includes('enterprise')
|
||||
}
|
||||
|
||||
private get isHTTPS(): boolean {
|
||||
const {source} = this.props
|
||||
return _.get(source, 'url', '').startsWith('https')
|
||||
}
|
||||
}
|
||||
|
||||
export default SourceForm
|
|
@ -0,0 +1,61 @@
|
|||
// Libraries
|
||||
import React, {SFC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import IndexList from 'src/shared/components/index_views/IndexList'
|
||||
import {Alignment} from 'src/clockface'
|
||||
import SourcesListRow from 'src/sources/components/SourcesListRow'
|
||||
|
||||
// Utils
|
||||
import {getSources} from 'src/sources/selectors'
|
||||
|
||||
// Styles
|
||||
import './SourcesList.scss'
|
||||
|
||||
// Types
|
||||
import {AppState, Source} from 'src/types/v2'
|
||||
|
||||
interface StateProps {
|
||||
sources: Source[]
|
||||
}
|
||||
|
||||
type Props = StateProps
|
||||
|
||||
const SourcesList: SFC<Props> = props => {
|
||||
const rows = props.sources.map(source => (
|
||||
<SourcesListRow key={source.id} source={source} />
|
||||
))
|
||||
|
||||
return (
|
||||
<div className="sources-list col-xs-12">
|
||||
<IndexList>
|
||||
<IndexList.Header>
|
||||
<IndexList.HeaderCell columnName="" width="10%" />
|
||||
<IndexList.HeaderCell columnName="Name" width="20%" />
|
||||
<IndexList.HeaderCell columnName="Type" width="10%" />
|
||||
<IndexList.HeaderCell columnName="URL" width="30%" />
|
||||
<IndexList.HeaderCell
|
||||
columnName=""
|
||||
width="30%"
|
||||
alignment={Alignment.Right}
|
||||
/>
|
||||
</IndexList.Header>
|
||||
<IndexList.Body emptyState={<div />} columnCount={4}>
|
||||
{rows}
|
||||
</IndexList.Body>
|
||||
</IndexList>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const sources = getSources(state)
|
||||
|
||||
return {sources}
|
||||
}
|
||||
|
||||
export default connect<StateProps, {}, {}>(
|
||||
mstp,
|
||||
null
|
||||
)(SourcesList)
|
|
@ -0,0 +1,3 @@
|
|||
.sources-list-row--connect-btn {
|
||||
width: 80px;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// Libraries
|
||||
import React, {SFC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import IndexList from 'src/shared/components/index_views/IndexList'
|
||||
import {Button, ComponentColor, ComponentSize} from 'src/clockface'
|
||||
import {Alignment} from 'src/clockface'
|
||||
import DeleteSourceButton from 'src/sources/components/DeleteSourceButton'
|
||||
|
||||
// Actions
|
||||
import {setActiveSource, deleteSource} from 'src/sources/actions'
|
||||
|
||||
// Styles
|
||||
import 'src/sources/components/SourcesListRow.scss'
|
||||
|
||||
// Types
|
||||
import {AppState} from 'src/types/v2'
|
||||
import {Source} from 'src/types/v2'
|
||||
|
||||
interface StateProps {
|
||||
activeSourceID: string
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onSetActiveSource: typeof setActiveSource
|
||||
onDeleteSource: (sourceID: string) => Promise<void>
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
source: Source
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps & OwnProps
|
||||
|
||||
const SourcesListRow: SFC<Props> = ({
|
||||
source,
|
||||
activeSourceID,
|
||||
onSetActiveSource,
|
||||
onDeleteSource,
|
||||
}) => {
|
||||
const canDelete = source.type !== 'self'
|
||||
const isActiveSource = source.id === activeSourceID
|
||||
const onButtonClick = () => onSetActiveSource(source.id)
|
||||
const onDeleteClick = () => onDeleteSource(source.id)
|
||||
|
||||
let buttonText
|
||||
let buttonColor
|
||||
|
||||
if (isActiveSource) {
|
||||
buttonText = 'Connected'
|
||||
buttonColor = ComponentColor.Success
|
||||
} else {
|
||||
buttonText = 'Connect'
|
||||
buttonColor = ComponentColor.Default
|
||||
}
|
||||
|
||||
return (
|
||||
<IndexList.Row
|
||||
key={source.id}
|
||||
disabled={false}
|
||||
customClass="sources-list-row"
|
||||
>
|
||||
<IndexList.Cell>
|
||||
<Button
|
||||
text={buttonText}
|
||||
color={buttonColor}
|
||||
size={ComponentSize.ExtraSmall}
|
||||
customClass="sources-list-row--connect-btn"
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
</IndexList.Cell>
|
||||
<IndexList.Cell>{source.name}</IndexList.Cell>
|
||||
<IndexList.Cell>{source.type}</IndexList.Cell>
|
||||
<IndexList.Cell>{source.url ? source.url : 'N/A'}</IndexList.Cell>
|
||||
<IndexList.Cell revealOnHover={true} alignment={Alignment.Right}>
|
||||
{canDelete && <DeleteSourceButton onClick={onDeleteClick} />}
|
||||
</IndexList.Cell>
|
||||
</IndexList.Row>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
return {
|
||||
activeSourceID: state.sources.activeSourceID,
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = dispatch => ({
|
||||
onSetActiveSource: activeSourceID =>
|
||||
dispatch(setActiveSource(activeSourceID)),
|
||||
onDeleteSource: sourceID => dispatch(deleteSource(sourceID)),
|
||||
})
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(SourcesListRow)
|
|
@ -0,0 +1,61 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Page} from 'src/pageLayout'
|
||||
import {
|
||||
Button,
|
||||
IconFont,
|
||||
ComponentColor,
|
||||
OverlayTechnology,
|
||||
} from 'src/clockface'
|
||||
import SourcesList from 'src/sources/components/SourcesList'
|
||||
import CreateSourceOverlay from 'src/sources/components/CreateSourceOverlay'
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
isAddingSource: boolean
|
||||
}
|
||||
|
||||
class SourcesPage extends PureComponent<Props, State> {
|
||||
public state: State = {isAddingSource: false}
|
||||
|
||||
public render() {
|
||||
const {isAddingSource} = this.state
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Page.Header>
|
||||
<Page.Header.Left>
|
||||
<Page.Title title="Manage Sources" />
|
||||
</Page.Header.Left>
|
||||
<Page.Header.Right>
|
||||
<Button
|
||||
text="Create Source"
|
||||
icon={IconFont.Plus}
|
||||
color={ComponentColor.Primary}
|
||||
onClick={this.handleShowOverlay}
|
||||
/>
|
||||
</Page.Header.Right>
|
||||
</Page.Header>
|
||||
<Page.Contents fullWidth={false} scrollable={true}>
|
||||
<SourcesList />
|
||||
<OverlayTechnology visible={isAddingSource}>
|
||||
<CreateSourceOverlay onHide={this.handleHideOverlay} />
|
||||
</OverlayTechnology>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
private handleShowOverlay = () => {
|
||||
this.setState({isAddingSource: true})
|
||||
}
|
||||
|
||||
private handleHideOverlay = () => {
|
||||
this.setState({isAddingSource: false})
|
||||
}
|
||||
}
|
||||
|
||||
export default SourcesPage
|
|
@ -1,77 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import * as sourcesActions from 'src/shared/actions/sources'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
import {Page} from 'src/pageLayout'
|
||||
import InfluxTable from 'src/sources/components/InfluxTable'
|
||||
|
||||
import {sourceDeleted, sourceDeleteFailed} from 'src/shared/copy/notifications'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
import {Notification, Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
sources: Source[]
|
||||
services: Service[]
|
||||
notify: (n: Notification) => void
|
||||
removeAndLoadSources: sourcesActions.RemoveAndLoadSources
|
||||
}
|
||||
|
||||
const VERSION = process.env.npm_package_version
|
||||
|
||||
@ErrorHandling
|
||||
class ManageSources extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {sources, source} = this.props
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Page.Header fullWidth={false}>
|
||||
<Page.Header.Left>
|
||||
<Page.Title title="Configuration" />
|
||||
</Page.Header.Left>
|
||||
<Page.Header.Right />
|
||||
</Page.Header>
|
||||
<Page.Contents fullWidth={false} scrollable={true}>
|
||||
<InfluxTable
|
||||
source={source}
|
||||
sources={sources}
|
||||
onDeleteSource={this.handleDeleteSource}
|
||||
/>
|
||||
<p className="version-number">Chronograf Version: {VERSION}</p>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
private handleDeleteSource = (source: Source) => {
|
||||
const {notify} = this.props
|
||||
|
||||
try {
|
||||
this.props.removeAndLoadSources(source)
|
||||
notify(sourceDeleted(source.name))
|
||||
} catch (e) {
|
||||
notify(sourceDeleteFailed(source.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = ({source, sources, services}) => ({
|
||||
source,
|
||||
sources,
|
||||
services,
|
||||
})
|
||||
|
||||
const mdtp = {
|
||||
notify: notifyAction,
|
||||
removeAndLoadSources: sourcesActions.removeAndLoadSources,
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mstp,
|
||||
mdtp
|
||||
)(ManageSources)
|
|
@ -1,261 +0,0 @@
|
|||
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react'
|
||||
import {withRouter, WithRouterProps} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import {createSource, updateSource} from 'src/sources/apis/v2'
|
||||
|
||||
import {
|
||||
addSource as addSourceAction,
|
||||
updateSource as updateSourceAction,
|
||||
AddSource,
|
||||
UpdateSource,
|
||||
} from 'src/shared/actions/sources'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
import Notifications from 'src/shared/components/notifications/Notifications'
|
||||
import SourceForm from 'src/sources/components/SourceForm'
|
||||
import {Page, PageHeader, PageContents} from 'src/pageLayout'
|
||||
import {DEFAULT_SOURCE} from 'src/shared/constants'
|
||||
|
||||
const INITIAL_PATH = '/sources/new'
|
||||
|
||||
import {
|
||||
sourceUpdated,
|
||||
sourceUpdateFailed,
|
||||
sourceCreationFailed,
|
||||
sourceCreationSucceeded,
|
||||
} from 'src/shared/copy/notifications'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {Source} from 'src/types/v2'
|
||||
import * as NotificationsActions from 'src/types/actions/notifications'
|
||||
|
||||
interface Props extends WithRouterProps {
|
||||
notify: NotificationsActions.PublishNotificationActionCreator
|
||||
addSource: AddSource
|
||||
updateSource: UpdateSource
|
||||
sourcesLink: string
|
||||
sources: Source[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCreated: boolean
|
||||
source: Partial<Source>
|
||||
editMode: boolean
|
||||
isInitialSource: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SourcePage extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isCreated: false,
|
||||
source: DEFAULT_SOURCE,
|
||||
editMode: props.params.id !== undefined,
|
||||
isInitialSource: props.router.location.pathname === INITIAL_PATH,
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.setState({
|
||||
source: this.source,
|
||||
})
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {source, editMode, isInitialSource} = this.state
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Notifications />
|
||||
<PageHeader fullWidth={false}>
|
||||
<PageHeader.Left>
|
||||
<h1 className="page--title">{this.pageTitle}</h1>
|
||||
</PageHeader.Left>
|
||||
<PageHeader.Right />
|
||||
</PageHeader>
|
||||
<PageContents fullWidth={false} scrollable={true}>
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="panel">
|
||||
<SourceForm
|
||||
source={source}
|
||||
editMode={editMode}
|
||||
onInputChange={this.handleInputChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
onBlurSourceURL={this.handleBlurSourceURL}
|
||||
isInitialSource={isInitialSource}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PageContents>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
private get source(): Partial<Source> {
|
||||
const {sources, params} = this.props
|
||||
const source = sources.find(s => s.id === params.id) || {}
|
||||
return {...DEFAULT_SOURCE, ...source}
|
||||
}
|
||||
|
||||
private handleSubmit = (e: MouseEvent<HTMLFormElement>): void => {
|
||||
e.preventDefault()
|
||||
const {isCreated, editMode} = this.state
|
||||
const isNewSource = !editMode
|
||||
if (!isCreated && isNewSource) {
|
||||
return this.setState(this.normalizeSource, this.createSource)
|
||||
}
|
||||
|
||||
this.setState(this.normalizeSource, this.updateSource)
|
||||
}
|
||||
|
||||
private normalizeSource({source}) {
|
||||
const url = source.url.trim()
|
||||
if (source.url.startsWith('http')) {
|
||||
return {source: {...source, url}}
|
||||
}
|
||||
|
||||
return {source: {...source, url: `http://${url}`}}
|
||||
}
|
||||
|
||||
private createSourceOnBlur = async () => {
|
||||
const {source} = this.state
|
||||
const {sourcesLink} = this.props
|
||||
// if there is a type on source it has already been created
|
||||
if (source.type) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceFromServer = await createSource(sourcesLink, source)
|
||||
this.props.addSource(sourceFromServer)
|
||||
this.setState({
|
||||
source: {...DEFAULT_SOURCE, ...sourceFromServer},
|
||||
isCreated: true,
|
||||
})
|
||||
} catch (err) {
|
||||
// dont want to flash this until they submit
|
||||
const error = this.parseError(err)
|
||||
console.error('Error creating InfluxDB connection: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
private createSource = async () => {
|
||||
const {source} = this.state
|
||||
const {notify, sourcesLink} = this.props
|
||||
|
||||
try {
|
||||
const sourceFromServer = await createSource(sourcesLink, source)
|
||||
this.props.addSource(sourceFromServer)
|
||||
this.redirect(sourceFromServer)
|
||||
notify(sourceCreationSucceeded(source.name))
|
||||
} catch (err) {
|
||||
// dont want to flash this until they submit
|
||||
notify(sourceCreationFailed(source.name, this.parseError(err)))
|
||||
}
|
||||
}
|
||||
|
||||
private updateSource = async () => {
|
||||
const {source} = this.state
|
||||
const {notify} = this.props
|
||||
try {
|
||||
const sourceFromServer = await updateSource(source)
|
||||
this.props.updateSource(sourceFromServer)
|
||||
this.redirect(sourceFromServer)
|
||||
notify(sourceUpdated(source.name))
|
||||
} catch (error) {
|
||||
notify(sourceUpdateFailed(source.name, this.parseError(error)))
|
||||
}
|
||||
}
|
||||
|
||||
private redirect = source => {
|
||||
const {isInitialSource} = this.state
|
||||
const {router, location} = this.props
|
||||
const sourceID = location.query.sourceID
|
||||
|
||||
if (isInitialSource) {
|
||||
return this.redirectToApp(source)
|
||||
}
|
||||
|
||||
router.push(`/manage-sources?sourceID=${sourceID}`)
|
||||
}
|
||||
|
||||
private parseError = (error): string => {
|
||||
return _.get(error, ['data', 'message'], error)
|
||||
}
|
||||
|
||||
private redirectToApp = source => {
|
||||
const {location, router} = this.props
|
||||
const {redirectPath} = location.query
|
||||
|
||||
if (!redirectPath) {
|
||||
return router.push(`/sources/${source.id}/hosts`)
|
||||
}
|
||||
|
||||
const fixedPath = redirectPath.replace(
|
||||
/\/sources\/[^/]*/,
|
||||
`/sources/${source.id}`
|
||||
)
|
||||
return router.push(fixedPath)
|
||||
}
|
||||
|
||||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
let val = e.target.value
|
||||
const name = e.target.name
|
||||
|
||||
if (e.target.type === 'checkbox') {
|
||||
val = e.target.checked as any
|
||||
}
|
||||
|
||||
this.setState(prevState => {
|
||||
const source = {
|
||||
...prevState.source,
|
||||
[name]: val,
|
||||
}
|
||||
|
||||
return {...prevState, source}
|
||||
})
|
||||
}
|
||||
|
||||
private handleBlurSourceURL = () => {
|
||||
const {source, editMode} = this.state
|
||||
if (editMode) {
|
||||
this.setState(this.normalizeSource)
|
||||
return
|
||||
}
|
||||
|
||||
if (!source.url) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState(this.normalizeSource, this.createSourceOnBlur)
|
||||
}
|
||||
|
||||
private get pageTitle(): string {
|
||||
const {editMode} = this.state
|
||||
|
||||
if (editMode) {
|
||||
return 'Configure InfluxDB Connection'
|
||||
}
|
||||
|
||||
return 'Add a New InfluxDB Connection'
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
notify: notifyAction,
|
||||
addSource: addSourceAction,
|
||||
updateSource: updateSourceAction,
|
||||
}
|
||||
|
||||
const mstp = ({links, sources}) => ({
|
||||
sourcesLink: links.sources,
|
||||
sources,
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mstp,
|
||||
mdtp
|
||||
)(withRouter(SourcePage))
|
|
@ -1,3 +0,0 @@
|
|||
import SourcePage from './containers/SourcePage'
|
||||
import ManageSources from './containers/ManageSources'
|
||||
export {SourcePage, ManageSources}
|
|
@ -0,0 +1,55 @@
|
|||
import {Source} from 'src/types/v2'
|
||||
import {Action} from 'src/sources/actions'
|
||||
|
||||
export interface SourcesState {
|
||||
activeSourceID: string | null
|
||||
sources: {
|
||||
[sourceID: string]: Source
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: SourcesState = {
|
||||
activeSourceID: null,
|
||||
sources: {},
|
||||
}
|
||||
|
||||
const sourcesReducer = (
|
||||
state: SourcesState = initialState,
|
||||
action: Action
|
||||
): SourcesState => {
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_SOURCE': {
|
||||
const {activeSourceID} = action.payload
|
||||
|
||||
return {...state, activeSourceID}
|
||||
}
|
||||
|
||||
case 'SET_SOURCES': {
|
||||
const sources = {...state.sources}
|
||||
|
||||
for (const source of action.payload.sources) {
|
||||
sources[source.id] = source
|
||||
}
|
||||
|
||||
return {...state, sources}
|
||||
}
|
||||
|
||||
case 'SET_SOURCE': {
|
||||
const {source} = action.payload
|
||||
|
||||
return {...state, sources: {...state.sources, [source.id]: source}}
|
||||
}
|
||||
|
||||
case 'REMOVE_SOURCE': {
|
||||
const sources = {...state.sources}
|
||||
|
||||
delete sources[action.payload.sourceID]
|
||||
|
||||
return {...state, sources}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export default sourcesReducer
|
|
@ -1,74 +0,0 @@
|
|||
import reducer, {initialState} from 'src/sources/reducers/sources'
|
||||
|
||||
import {updateSource, addSource, loadSources} from 'src/shared/actions/sources'
|
||||
|
||||
import {source} from 'src/sources/resources'
|
||||
|
||||
describe('sources reducer', () => {
|
||||
it('can LOAD_SOURCES', () => {
|
||||
const expected = [{...source, id: '1'}]
|
||||
const actual = reducer(initialState, loadSources(expected))
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
describe('ADD_SOURCES', () => {
|
||||
it('can ADD_SOURCES', () => {
|
||||
let state = []
|
||||
|
||||
state = reducer(
|
||||
state,
|
||||
addSource({
|
||||
...source,
|
||||
id: '1',
|
||||
default: true,
|
||||
})
|
||||
)
|
||||
|
||||
state = reducer(
|
||||
state,
|
||||
addSource({
|
||||
...source,
|
||||
id: '2',
|
||||
default: true,
|
||||
})
|
||||
)
|
||||
|
||||
expect(state.filter(s => s.default).length).toBe(1)
|
||||
})
|
||||
|
||||
it('can correctly show default sources when updating a source', () => {
|
||||
let state = []
|
||||
|
||||
state = reducer(
|
||||
initialState,
|
||||
addSource({
|
||||
...source,
|
||||
id: '1',
|
||||
default: true,
|
||||
})
|
||||
)
|
||||
|
||||
state = reducer(
|
||||
state,
|
||||
addSource({
|
||||
...source,
|
||||
id: '2',
|
||||
default: true,
|
||||
})
|
||||
)
|
||||
|
||||
state = reducer(
|
||||
state,
|
||||
updateSource({
|
||||
...source,
|
||||
id: '1',
|
||||
default: true,
|
||||
})
|
||||
)
|
||||
|
||||
expect(state.find(({id}) => id === '1').default).toBe(true)
|
||||
expect(state.find(({id}) => id === '2').default).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,40 +0,0 @@
|
|||
import {Source} from 'src/types/v2'
|
||||
import {Action} from 'src/shared/actions/sources'
|
||||
|
||||
export const initialState: Source[] = []
|
||||
|
||||
const sourcesReducer = (state = initialState, action: Action): Source[] => {
|
||||
switch (action.type) {
|
||||
case 'LOAD_SOURCES': {
|
||||
return action.payload.sources
|
||||
}
|
||||
|
||||
case 'SOURCE_UPDATED': {
|
||||
const {source} = action.payload
|
||||
const updatedIndex = state.findIndex(s => s.id === source.id)
|
||||
const updatedSources = source.default
|
||||
? state.map(s => {
|
||||
s.default = false
|
||||
return s
|
||||
})
|
||||
: [...state]
|
||||
updatedSources[updatedIndex] = source
|
||||
return updatedSources
|
||||
}
|
||||
|
||||
case 'SOURCE_ADDED': {
|
||||
const {source} = action.payload
|
||||
const updatedSources = source.default
|
||||
? state.map(s => {
|
||||
s.default = false
|
||||
return s
|
||||
})
|
||||
: state
|
||||
return [...updatedSources, source]
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export default sourcesReducer
|
|
@ -1,21 +0,0 @@
|
|||
import {Source} from 'src/types/v2'
|
||||
import {SourceLinks, SourceAuthenticationMethod} from 'src/types/v2/sources'
|
||||
|
||||
export const sourceLinks: SourceLinks = {
|
||||
query: '/v2/sources/16/query',
|
||||
self: '/v2/sources/16',
|
||||
health: '/v2/sources/16/health',
|
||||
buckets: '/v2/sources/16/buckets',
|
||||
}
|
||||
export const source: Source = {
|
||||
id: '16',
|
||||
name: 'ssl',
|
||||
type: 'influx',
|
||||
username: 'admin',
|
||||
url: 'https://localhost:9086',
|
||||
insecureSkipVerify: true,
|
||||
default: false,
|
||||
telegraf: 'telegraf',
|
||||
links: sourceLinks,
|
||||
authentication: SourceAuthenticationMethod.Basic,
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import {AppState, Source} from 'src/types/v2'
|
||||
|
||||
export const getActiveSource = (state: AppState): Source | null => {
|
||||
const {activeSourceID} = state.sources
|
||||
|
||||
if (!activeSourceID) {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = state.sources.sources[activeSourceID]
|
||||
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export const getSources = (state: AppState): Source[] =>
|
||||
Object.values(state.sources.sources)
|
|
@ -8,7 +8,7 @@ import {resizeLayout} from 'src/shared/middleware/resizeLayout'
|
|||
import {queryStringConfig} from 'src/shared/middleware/queryStringConfig'
|
||||
import sharedReducers from 'src/shared/reducers'
|
||||
import persistStateEnhancer from './persistStateEnhancer'
|
||||
import sourcesReducer from 'src/sources/reducers/sources'
|
||||
import sourcesReducer from 'src/sources/reducers'
|
||||
|
||||
// v2 reducers
|
||||
import meReducer from 'src/shared/reducers/v2/me'
|
||||
|
@ -19,7 +19,6 @@ import viewsReducer from 'src/dashboards/reducers/v2/views'
|
|||
import logsReducer from 'src/logs/reducers'
|
||||
import timeMachinesReducer from 'src/shared/reducers/v2/timeMachines'
|
||||
import orgsReducer from 'src/organizations/reducers/orgs'
|
||||
import sourceReducer from 'src/shared/reducers/v2/source'
|
||||
|
||||
// Types
|
||||
import {LocalStorage} from 'src/types/localStorage'
|
||||
|
@ -42,7 +41,6 @@ const rootReducer = combineReducers<ReducerState>({
|
|||
tasks: tasksReducer,
|
||||
orgs: orgsReducer,
|
||||
me: meReducer,
|
||||
source: sourceReducer,
|
||||
})
|
||||
|
||||
const composeEnhancers =
|
||||
|
|
|
@ -28,8 +28,8 @@ import {AppState as AppPresentationState} from 'src/shared/reducers/app'
|
|||
import {State as TaskState} from 'src/tasks/reducers/v2'
|
||||
import {RouterState} from 'react-router-redux'
|
||||
import {MeState} from 'src/shared/reducers/v2/me'
|
||||
import {SourceState} from 'src/shared/reducers/v2/source'
|
||||
import {OverlayState} from 'src/types/v2/overlay'
|
||||
import {SourcesState} from 'src/sources/reducers'
|
||||
|
||||
export interface AppState {
|
||||
VERSION: string
|
||||
|
@ -38,7 +38,7 @@ export interface AppState {
|
|||
logs: LogsState
|
||||
ranges: RangeState
|
||||
views: ViewsState
|
||||
sources: Source[]
|
||||
sources: SourcesState
|
||||
dashboards: Dashboard[]
|
||||
notifications: Notification[]
|
||||
timeMachines: TimeMachinesState
|
||||
|
@ -47,9 +47,10 @@ export interface AppState {
|
|||
timeRange: TimeRange
|
||||
orgs: Organization[]
|
||||
me: MeState
|
||||
source: SourceState
|
||||
}
|
||||
|
||||
export type GetState = () => AppState
|
||||
|
||||
export {
|
||||
User,
|
||||
UserToken,
|
||||
|
|
Loading…
Reference in New Issue