Add initial source management UI
parent
71b2eec624
commit
81c0e53c4a
|
@ -251,7 +251,9 @@ func (h *SourceHandler) handlePostSource(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
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)
|
EncodeError(ctx, err, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {getBasepath} from 'src/utils/basepath'
|
||||||
// Components
|
// Components
|
||||||
import App from 'src/App'
|
import App from 'src/App'
|
||||||
import GetSources from 'src/shared/containers/GetSources'
|
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 GetOrganizations from 'src/shared/containers/GetOrganizations'
|
||||||
import Setup from 'src/Setup'
|
import Setup from 'src/Setup'
|
||||||
import Signin from 'src/Signin'
|
import Signin from 'src/Signin'
|
||||||
|
@ -26,12 +26,12 @@ import OrganizationView from 'src/organizations/containers/OrganizationView'
|
||||||
import TaskEditPage from 'src/tasks/containers/TaskEditPage'
|
import TaskEditPage from 'src/tasks/containers/TaskEditPage'
|
||||||
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
||||||
import DataExplorerPage from 'src/dataExplorer/components/DataExplorerPage'
|
import DataExplorerPage from 'src/dataExplorer/components/DataExplorerPage'
|
||||||
import {SourcePage, ManageSources} from 'src/sources'
|
|
||||||
import {UserPage} from 'src/user'
|
import {UserPage} from 'src/user'
|
||||||
import {LogsPage} from 'src/logs'
|
import {LogsPage} from 'src/logs'
|
||||||
import NotFound from 'src/shared/components/NotFound'
|
import NotFound from 'src/shared/components/NotFound'
|
||||||
import GetLinks from 'src/shared/containers/GetLinks'
|
import GetLinks from 'src/shared/containers/GetLinks'
|
||||||
import GetMe from 'src/shared/containers/GetMe'
|
import GetMe from 'src/shared/containers/GetMe'
|
||||||
|
import SourcesPage from 'src/sources/components/SourcesPage'
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {disablePresentationMode} from 'src/shared/actions/app'
|
import {disablePresentationMode} from 'src/shared/actions/app'
|
||||||
|
@ -82,7 +82,7 @@ class Root extends PureComponent {
|
||||||
<Route component={GetOrganizations}>
|
<Route component={GetOrganizations}>
|
||||||
<Route component={App}>
|
<Route component={App}>
|
||||||
<Route path="/" component={GetSources}>
|
<Route path="/" component={GetSources}>
|
||||||
<Route path="/" component={SetSource}>
|
<Route path="/" component={SetActiveSource}>
|
||||||
<Route
|
<Route
|
||||||
path="dashboards/:dashboardID"
|
path="dashboards/:dashboardID"
|
||||||
component={DashboardPage}
|
component={DashboardPage}
|
||||||
|
@ -98,26 +98,14 @@ class Root extends PureComponent {
|
||||||
/>
|
/>
|
||||||
<Route path="tasks/new" component={TaskPage} />
|
<Route path="tasks/new" component={TaskPage} />
|
||||||
<Route path="tasks/:id" component={TaskEditPage} />
|
<Route path="tasks/:id" component={TaskEditPage} />
|
||||||
<Route path="sources/new" component={SourcePage} />
|
|
||||||
<Route
|
<Route
|
||||||
path="data-explorer"
|
path="data-explorer"
|
||||||
component={DataExplorerPage}
|
component={DataExplorerPage}
|
||||||
/>
|
/>
|
||||||
<Route path="dashboards" component={DashboardsPage} />
|
<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="user_profile" component={UserPage} />
|
||||||
<Route path="logs" component={LogsPage} />
|
<Route path="logs" component={LogsPage} />
|
||||||
|
<Route path="sources" component={SourcesPage} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
createView as createViewAJAX,
|
createView as createViewAJAX,
|
||||||
updateView as updateViewAJAX,
|
updateView as updateViewAJAX,
|
||||||
} from 'src/dashboards/apis/v2/view'
|
} 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 {getBuckets} from 'src/shared/apis/v2/buckets'
|
||||||
import {executeQueryAsync} from 'src/logs/api/v2'
|
import {executeQueryAsync} from 'src/logs/api/v2'
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@ export const populateBucketsAsync = (
|
||||||
export const getSourceAndPopulateBucketsAsync = (sourceURL: string) => async (
|
export const getSourceAndPopulateBucketsAsync = (sourceURL: string) => async (
|
||||||
dispatch
|
dispatch
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const source = await getSource(sourceURL)
|
const source = await readSource(sourceURL)
|
||||||
|
|
||||||
const bucketsLink = getDeep<string | null>(source, 'links.buckets', null)
|
const bucketsLink = getDeep<string | null>(source, 'links.buckets', null)
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ import LogsTable from 'src/logs/components/logs_table/LogsTable'
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import * as logActions from 'src/logs/actions'
|
import * as logActions from 'src/logs/actions'
|
||||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
|
||||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
@ -24,6 +23,7 @@ import {
|
||||||
applyChangesToTableData,
|
applyChangesToTableData,
|
||||||
isEmptyInfiniteData,
|
isEmptyInfiniteData,
|
||||||
} from 'src/logs/utils/table'
|
} from 'src/logs/utils/table'
|
||||||
|
import {getSources} from 'src/sources/selectors'
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import {NOW, DEFAULT_TAIL_CHUNK_DURATION_MS} from 'src/logs/constants'
|
import {NOW, DEFAULT_TAIL_CHUNK_DURATION_MS} from 'src/logs/constants'
|
||||||
|
@ -57,7 +57,6 @@ interface TableConfigStateProps {
|
||||||
interface DispatchTableConfigProps {
|
interface DispatchTableConfigProps {
|
||||||
notify: typeof notifyAction
|
notify: typeof notifyAction
|
||||||
getConfig: typeof logActions.getLogConfigAsync
|
getConfig: typeof logActions.getLogConfigAsync
|
||||||
getSources: typeof getSourcesAsync
|
|
||||||
addFilter: typeof logActions.addFilter // TODO: update addFilters
|
addFilter: typeof logActions.addFilter // TODO: update addFilters
|
||||||
setConfig: typeof logActions.setConfig
|
setConfig: typeof logActions.setConfig
|
||||||
updateConfig: typeof logActions.updateLogConfigAsync
|
updateConfig: typeof logActions.updateLogConfigAsync
|
||||||
|
@ -120,7 +119,6 @@ class LogsPage extends Component<Props, State> {
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
try {
|
try {
|
||||||
await this.props.getSources()
|
|
||||||
await this.setCurrentSource()
|
await this.setCurrentSource()
|
||||||
await this.props.getConfig(this.configLink)
|
await this.props.getConfig(this.configLink)
|
||||||
|
|
||||||
|
@ -558,37 +556,41 @@ class LogsPage extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mstp = ({
|
const mstp = (state: AppState): StateProps => {
|
||||||
sources,
|
const {
|
||||||
links,
|
links,
|
||||||
logs: {
|
logs: {
|
||||||
currentSource,
|
currentSource,
|
||||||
currentBuckets,
|
currentBuckets,
|
||||||
currentBucket,
|
currentBucket,
|
||||||
|
filters,
|
||||||
|
logConfig,
|
||||||
|
searchStatus,
|
||||||
|
tableInfiniteData,
|
||||||
|
nextTailLowerBound,
|
||||||
|
currentTailUpperBound,
|
||||||
|
},
|
||||||
|
} = state
|
||||||
|
|
||||||
|
const sources = getSources(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
links,
|
||||||
|
sources,
|
||||||
filters,
|
filters,
|
||||||
logConfig,
|
logConfig,
|
||||||
searchStatus,
|
searchStatus,
|
||||||
|
currentSource,
|
||||||
|
currentBucket,
|
||||||
|
currentBuckets,
|
||||||
tableInfiniteData,
|
tableInfiniteData,
|
||||||
nextTailLowerBound,
|
nextTailLowerBound,
|
||||||
currentTailUpperBound,
|
currentTailUpperBound,
|
||||||
},
|
}
|
||||||
}: AppState): StateProps => ({
|
}
|
||||||
links,
|
|
||||||
sources,
|
|
||||||
filters,
|
|
||||||
logConfig,
|
|
||||||
searchStatus,
|
|
||||||
currentSource,
|
|
||||||
currentBucket,
|
|
||||||
currentBuckets,
|
|
||||||
tableInfiniteData,
|
|
||||||
nextTailLowerBound,
|
|
||||||
currentTailUpperBound,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mdtp: DispatchProps = {
|
const mdtp: DispatchProps = {
|
||||||
notify: notifyAction,
|
notify: notifyAction,
|
||||||
getSources: getSourcesAsync,
|
|
||||||
addFilter: logActions.addFilter,
|
addFilter: logActions.addFilter,
|
||||||
updateConfig: logActions.updateLogConfigAsync,
|
updateConfig: logActions.updateLogConfigAsync,
|
||||||
createConfig: logActions.createLogConfigAsync,
|
createConfig: logActions.createLogConfigAsync,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {Component} from 'react'
|
import React, {Component, ReactNode} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
@ -9,7 +9,7 @@ import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: JSX.Element[] | JSX.Element
|
children: JSX.Element[] | JSX.Element | ReactNode
|
||||||
fullWidth: boolean
|
fullWidth: boolean
|
||||||
scrollable: boolean
|
scrollable: boolean
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ class PageContents extends Component<Props> {
|
||||||
return classnames('page-contents', {'full-width': fullWidth})
|
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
|
const {fullWidth, children} = this.props
|
||||||
|
|
||||||
if (fullWidth) {
|
if (fullWidth) {
|
||||||
|
|
|
@ -7,8 +7,11 @@ import _ from 'lodash'
|
||||||
// Components
|
// Components
|
||||||
import NavMenu from 'src/pageLayout/components/NavMenu'
|
import NavMenu from 'src/pageLayout/components/NavMenu'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import {getSources} from 'src/sources/selectors'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {Source} from 'src/types/v2'
|
import {Source, AppState} from 'src/types/v2'
|
||||||
import {IconFont} from 'src/clockface'
|
import {IconFont} from 'src/clockface'
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
|
@ -62,7 +65,7 @@ class SideNav extends PureComponent<Props> {
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
link: `/${this.sourceParam}`,
|
link: '/',
|
||||||
icon: IconFont.Cubouniform,
|
icon: IconFont.Cubouniform,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['status'],
|
highlightWhen: ['status'],
|
||||||
|
@ -70,7 +73,7 @@ class SideNav extends PureComponent<Props> {
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Data Explorer',
|
title: 'Data Explorer',
|
||||||
link: `/data-explorer/${this.sourceParam}`,
|
link: '/data-explorer',
|
||||||
icon: IconFont.Capacitor,
|
icon: IconFont.Capacitor,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['data-explorer'],
|
highlightWhen: ['data-explorer'],
|
||||||
|
@ -78,7 +81,7 @@ class SideNav extends PureComponent<Props> {
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Dashboards',
|
title: 'Dashboards',
|
||||||
link: `/dashboards/${this.sourceParam}`,
|
link: '/dashboards',
|
||||||
icon: IconFont.DashJ,
|
icon: IconFont.DashJ,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['dashboards'],
|
highlightWhen: ['dashboards'],
|
||||||
|
@ -86,7 +89,7 @@ class SideNav extends PureComponent<Props> {
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Logs',
|
title: 'Logs',
|
||||||
link: `/logs/${this.sourceParam}`,
|
link: '/logs',
|
||||||
icon: IconFont.Wood,
|
icon: IconFont.Wood,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['logs'],
|
highlightWhen: ['logs'],
|
||||||
|
@ -94,7 +97,7 @@ class SideNav extends PureComponent<Props> {
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Tasks',
|
title: 'Tasks',
|
||||||
link: `/tasks/${this.sourceParam}`,
|
link: '/tasks',
|
||||||
icon: IconFont.Alerts,
|
icon: IconFont.Alerts,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['tasks'],
|
highlightWhen: ['tasks'],
|
||||||
|
@ -102,49 +105,36 @@ class SideNav extends PureComponent<Props> {
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Organizations',
|
title: 'Organizations',
|
||||||
link: `/organizations/${this.sourceParam}`,
|
link: '/organizations',
|
||||||
icon: IconFont.Group,
|
icon: IconFont.Group,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['organizations'],
|
highlightWhen: ['organizations'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: NavItemType.Icon,
|
type: NavItemType.Icon,
|
||||||
title: 'Configuration',
|
title: 'Sources',
|
||||||
link: `/manage-sources/${this.sourceParam}`,
|
link: '/sources',
|
||||||
icon: IconFont.Wrench,
|
icon: IconFont.Wrench,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['manage-sources'],
|
highlightWhen: ['sources'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: NavItemType.Avatar,
|
type: NavItemType.Avatar,
|
||||||
title: 'My Profile',
|
title: 'My Profile',
|
||||||
link: `/user_profile/${this.sourceParam}`,
|
link: '/user_profile',
|
||||||
image: LeroyJenkins.avatar,
|
image: LeroyJenkins.avatar,
|
||||||
location: location.pathname,
|
location: location.pathname,
|
||||||
highlightWhen: ['user_profile'],
|
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 = ({
|
const mstp = (state: AppState) => {
|
||||||
sources,
|
const isHidden = state.app.ephemeral.inPresentationMode
|
||||||
app: {
|
const sources = getSources(state)
|
||||||
ephemeral: {inPresentationMode},
|
|
||||||
},
|
|
||||||
}) => ({
|
|
||||||
sources,
|
|
||||||
isHidden: inPresentationMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
// Constants
|
||||||
import {emptyGraphCopy} from 'src/shared/copy/cell'
|
import {emptyGraphCopy} from 'src/shared/copy/cell'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import {getActiveSource} from 'src/sources/selectors'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {TimeRange} from 'src/types'
|
import {TimeRange} from 'src/types'
|
||||||
import {AppState} from 'src/types/v2'
|
import {AppState} from 'src/types/v2'
|
||||||
|
@ -102,12 +105,10 @@ class RefreshingView extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mstp = ({source}: AppState): StateProps => {
|
const mstp = (state: AppState): StateProps => {
|
||||||
const link = source.links.query
|
const link = getActiveSource(state).links.query
|
||||||
|
|
||||||
return {
|
return {link}
|
||||||
link,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdtp = {}
|
const mdtp = {}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
children: JSX.Element[] | JSX.Element
|
children: JSX.Element[] | JSX.Element
|
||||||
|
customClass?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ErrorHandling
|
@ErrorHandling
|
||||||
|
@ -23,9 +24,12 @@ class IndexListRow extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get className(): string {
|
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'
|
import {source} from 'mocks/dummy'
|
||||||
|
|
||||||
jest.mock('src/sources/apis', () => require('mocks/sources/apis'))
|
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 setup = (override?) => {
|
||||||
const props = {
|
const props = {
|
||||||
getSources,
|
onReadSources,
|
||||||
sources: [source],
|
sources: [source],
|
||||||
router: {},
|
router: {},
|
||||||
location: {
|
location: {
|
||||||
|
|
|
@ -4,12 +4,12 @@ import {connect} from 'react-redux'
|
||||||
|
|
||||||
import {RemoteDataState} from 'src/types'
|
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'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactElement<any>
|
children: React.ReactElement<any>
|
||||||
getSources: typeof getSourcesAsync
|
onReadSources: typeof readSources
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -27,7 +27,8 @@ export class GetSources extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
await this.props.getSources()
|
await this.props.onReadSources()
|
||||||
|
|
||||||
this.setState({ready: RemoteDataState.Done})
|
this.setState({ready: RemoteDataState.Done})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ export class GetSources extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdtp = {
|
const mdtp = {
|
||||||
getSources: getSourcesAsync,
|
onReadSources: readSources,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
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.`,
|
message: `Connected to InfluxDB ${sourceName} successfully.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sourceCreationFailed = (
|
export const sourceCreationFailed = (errorMessage: string): Notification => ({
|
||||||
sourceName: string,
|
|
||||||
errorMessage: string
|
|
||||||
): Notification => ({
|
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'server2',
|
icon: 'server2',
|
||||||
message: `Unable to connect to InfluxDB ${sourceName}: ${errorMessage}`,
|
message: `Unable to create source: ${errorMessage}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sourceUpdated = (sourceName: string): Notification => ({
|
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 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 {
|
try {
|
||||||
await AJAX({url})
|
await AJAX({url})
|
||||||
} catch (error) {
|
} 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 {queryStringConfig} from 'src/shared/middleware/queryStringConfig'
|
||||||
import sharedReducers from 'src/shared/reducers'
|
import sharedReducers from 'src/shared/reducers'
|
||||||
import persistStateEnhancer from './persistStateEnhancer'
|
import persistStateEnhancer from './persistStateEnhancer'
|
||||||
import sourcesReducer from 'src/sources/reducers/sources'
|
import sourcesReducer from 'src/sources/reducers'
|
||||||
|
|
||||||
// v2 reducers
|
// v2 reducers
|
||||||
import meReducer from 'src/shared/reducers/v2/me'
|
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 logsReducer from 'src/logs/reducers'
|
||||||
import timeMachinesReducer from 'src/shared/reducers/v2/timeMachines'
|
import timeMachinesReducer from 'src/shared/reducers/v2/timeMachines'
|
||||||
import orgsReducer from 'src/organizations/reducers/orgs'
|
import orgsReducer from 'src/organizations/reducers/orgs'
|
||||||
import sourceReducer from 'src/shared/reducers/v2/source'
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {LocalStorage} from 'src/types/localStorage'
|
import {LocalStorage} from 'src/types/localStorage'
|
||||||
|
@ -42,7 +41,6 @@ const rootReducer = combineReducers<ReducerState>({
|
||||||
tasks: tasksReducer,
|
tasks: tasksReducer,
|
||||||
orgs: orgsReducer,
|
orgs: orgsReducer,
|
||||||
me: meReducer,
|
me: meReducer,
|
||||||
source: sourceReducer,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const composeEnhancers =
|
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 {State as TaskState} from 'src/tasks/reducers/v2'
|
||||||
import {RouterState} from 'react-router-redux'
|
import {RouterState} from 'react-router-redux'
|
||||||
import {MeState} from 'src/shared/reducers/v2/me'
|
import {MeState} from 'src/shared/reducers/v2/me'
|
||||||
import {SourceState} from 'src/shared/reducers/v2/source'
|
|
||||||
import {OverlayState} from 'src/types/v2/overlay'
|
import {OverlayState} from 'src/types/v2/overlay'
|
||||||
|
import {SourcesState} from 'src/sources/reducers'
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
VERSION: string
|
VERSION: string
|
||||||
|
@ -38,7 +38,7 @@ export interface AppState {
|
||||||
logs: LogsState
|
logs: LogsState
|
||||||
ranges: RangeState
|
ranges: RangeState
|
||||||
views: ViewsState
|
views: ViewsState
|
||||||
sources: Source[]
|
sources: SourcesState
|
||||||
dashboards: Dashboard[]
|
dashboards: Dashboard[]
|
||||||
notifications: Notification[]
|
notifications: Notification[]
|
||||||
timeMachines: TimeMachinesState
|
timeMachines: TimeMachinesState
|
||||||
|
@ -47,9 +47,10 @@ export interface AppState {
|
||||||
timeRange: TimeRange
|
timeRange: TimeRange
|
||||||
orgs: Organization[]
|
orgs: Organization[]
|
||||||
me: MeState
|
me: MeState
|
||||||
source: SourceState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetState = () => AppState
|
||||||
|
|
||||||
export {
|
export {
|
||||||
User,
|
User,
|
||||||
UserToken,
|
UserToken,
|
||||||
|
|
Loading…
Reference in New Issue