From 81c0e53c4a515668db7181b01f832d852f521109 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Tue, 13 Nov 2018 11:12:57 -0800 Subject: [PATCH] Add initial source management UI --- http/source_service.go | 4 +- ui/src/index.tsx | 20 +- ui/src/logs/actions/index.ts | 4 +- ui/src/logs/containers/logs_page/LogsPage.tsx | 52 ++-- ui/src/pageLayout/components/PageContents.tsx | 6 +- ui/src/pageLayout/containers/Nav.tsx | 52 ++-- ui/src/shared/actions/sources.ts | 90 ------ ui/src/shared/actions/v2/source.ts | 30 -- ui/src/shared/components/RefreshingView.tsx | 11 +- .../components/index_views/IndexListRow.tsx | 8 +- ui/src/shared/containers/GetSources.test.tsx | 5 +- ui/src/shared/containers/GetSources.tsx | 9 +- ui/src/shared/containers/SetActiveSource.tsx | 94 +++++++ ui/src/shared/containers/SetSource.tsx | 130 --------- ui/src/shared/copy/notifications.ts | 7 +- ui/src/shared/reducers/v2/source.ts | 27 -- ui/src/shared/utils/queryParams.ts | 27 ++ ui/src/sources/actions/index.ts | 112 ++++++++ ui/src/sources/apis/index.ts | 49 +++- ui/src/sources/apis/v2/index.ts | 75 ----- ui/src/sources/components/ConnectionLink.tsx | 59 ---- .../components/CreateSourceOverlay.scss | 11 + .../components/CreateSourceOverlay.tsx | 165 +++++++++++ .../sources/components/DeleteSourceButton.tsx | 53 ++++ ui/src/sources/components/InfluxTable.tsx | 51 ---- ui/src/sources/components/InfluxTableHead.tsx | 15 - .../sources/components/InfluxTableHeader.tsx | 33 --- ui/src/sources/components/InfluxTableRow.tsx | 79 ------ ui/src/sources/components/SourceForm.test.tsx | 42 --- ui/src/sources/components/SourceForm.tsx | 180 ------------ ui/src/sources/components/SourcesList.scss | 0 ui/src/sources/components/SourcesList.tsx | 61 ++++ ui/src/sources/components/SourcesListRow.scss | 3 + ui/src/sources/components/SourcesListRow.tsx | 98 +++++++ ui/src/sources/components/SourcesPage.tsx | 61 ++++ ui/src/sources/containers/ManageSources.tsx | 77 ------ ui/src/sources/containers/SourcePage.tsx | 261 ------------------ ui/src/sources/index.ts | 3 - ui/src/sources/reducers/index.ts | 55 ++++ ui/src/sources/reducers/sources.test.ts | 74 ----- ui/src/sources/reducers/sources.ts | 40 --- ui/src/sources/resources.ts | 21 -- ui/src/sources/selectors/index.ts | 20 ++ ui/src/store/configureStore.ts | 4 +- ui/src/types/v2/index.ts | 7 +- 45 files changed, 895 insertions(+), 1390 deletions(-) delete mode 100644 ui/src/shared/actions/sources.ts delete mode 100644 ui/src/shared/actions/v2/source.ts create mode 100644 ui/src/shared/containers/SetActiveSource.tsx delete mode 100644 ui/src/shared/containers/SetSource.tsx delete mode 100644 ui/src/shared/reducers/v2/source.ts create mode 100644 ui/src/shared/utils/queryParams.ts create mode 100644 ui/src/sources/actions/index.ts delete mode 100644 ui/src/sources/apis/v2/index.ts delete mode 100644 ui/src/sources/components/ConnectionLink.tsx create mode 100644 ui/src/sources/components/CreateSourceOverlay.scss create mode 100644 ui/src/sources/components/CreateSourceOverlay.tsx create mode 100644 ui/src/sources/components/DeleteSourceButton.tsx delete mode 100644 ui/src/sources/components/InfluxTable.tsx delete mode 100644 ui/src/sources/components/InfluxTableHead.tsx delete mode 100644 ui/src/sources/components/InfluxTableHeader.tsx delete mode 100644 ui/src/sources/components/InfluxTableRow.tsx delete mode 100644 ui/src/sources/components/SourceForm.test.tsx delete mode 100644 ui/src/sources/components/SourceForm.tsx create mode 100644 ui/src/sources/components/SourcesList.scss create mode 100644 ui/src/sources/components/SourcesList.tsx create mode 100644 ui/src/sources/components/SourcesListRow.scss create mode 100644 ui/src/sources/components/SourcesListRow.tsx create mode 100644 ui/src/sources/components/SourcesPage.tsx delete mode 100644 ui/src/sources/containers/ManageSources.tsx delete mode 100644 ui/src/sources/containers/SourcePage.tsx delete mode 100644 ui/src/sources/index.ts create mode 100644 ui/src/sources/reducers/index.ts delete mode 100644 ui/src/sources/reducers/sources.test.ts delete mode 100644 ui/src/sources/reducers/sources.ts delete mode 100644 ui/src/sources/resources.ts create mode 100644 ui/src/sources/selectors/index.ts diff --git a/http/source_service.go b/http/source_service.go index 6b4dbfeded..44e3288ea0 100644 --- a/http/source_service.go +++ b/http/source_service.go @@ -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 } diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 83e6c6e0e2..a7243cb79c 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -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 { - + - - - - + diff --git a/ui/src/logs/actions/index.ts b/ui/src/logs/actions/index.ts index e7bd8e8421..28a5313fdc 100644 --- a/ui/src/logs/actions/index.ts +++ b/ui/src/logs/actions/index.ts @@ -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 => { - const source = await getSource(sourceURL) + const source = await readSource(sourceURL) const bucketsLink = getDeep(source, 'links.buckets', null) diff --git a/ui/src/logs/containers/logs_page/LogsPage.tsx b/ui/src/logs/containers/logs_page/LogsPage.tsx index bfad72ec6b..ff775a65bb 100644 --- a/ui/src/logs/containers/logs_page/LogsPage.tsx +++ b/ui/src/logs/containers/logs_page/LogsPage.tsx @@ -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 { public async componentDidMount() { try { - await this.props.getSources() await this.setCurrentSource() await this.props.getConfig(this.configLink) @@ -558,37 +556,41 @@ class LogsPage extends Component { } } -const mstp = ({ - sources, - links, - logs: { - currentSource, - currentBuckets, - currentBucket, +const mstp = (state: AppState): StateProps => { + const { + links, + logs: { + currentSource, + currentBuckets, + currentBucket, + filters, + logConfig, + searchStatus, + tableInfiniteData, + nextTailLowerBound, + currentTailUpperBound, + }, + } = state + + const sources = getSources(state) + + return { + links, + sources, filters, logConfig, searchStatus, + currentSource, + currentBucket, + currentBuckets, tableInfiniteData, nextTailLowerBound, currentTailUpperBound, - }, -}: AppState): StateProps => ({ - links, - sources, - filters, - logConfig, - searchStatus, - currentSource, - currentBucket, - currentBuckets, - tableInfiniteData, - nextTailLowerBound, - currentTailUpperBound, -}) + } +} const mdtp: DispatchProps = { notify: notifyAction, - getSources: getSourcesAsync, addFilter: logActions.addFilter, updateConfig: logActions.updateLogConfigAsync, createConfig: logActions.createLogConfigAsync, diff --git a/ui/src/pageLayout/components/PageContents.tsx b/ui/src/pageLayout/components/PageContents.tsx index 97e81ed67f..e156000f77 100644 --- a/ui/src/pageLayout/components/PageContents.tsx +++ b/ui/src/pageLayout/components/PageContents.tsx @@ -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 { 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) { diff --git a/ui/src/pageLayout/containers/Nav.tsx b/ui/src/pageLayout/containers/Nav.tsx index 7299dd7e38..20c858f5b0 100644 --- a/ui/src/pageLayout/containers/Nav.tsx +++ b/ui/src/pageLayout/containers/Nav.tsx @@ -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 { { 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 { { 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 { { 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 { { 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 { { 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 { { 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)) diff --git a/ui/src/shared/actions/sources.ts b/ui/src/shared/actions/sources.ts deleted file mode 100644 index 57592e51e7..0000000000 --- a/ui/src/shared/actions/sources.ts +++ /dev/null @@ -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 -// Async action creators -export const removeAndLoadSources = (source: Source) => async ( - dispatch -): Promise => { - 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 => { - try { - const sources = await getSourcesAJAX() - dispatch(loadSources(sources)) - } catch (error) { - dispatch(notify(serverError)) - } -} diff --git a/ui/src/shared/actions/v2/source.ts b/ui/src/shared/actions/v2/source.ts deleted file mode 100644 index 6efac89da8..0000000000 --- a/ui/src/shared/actions/v2/source.ts +++ /dev/null @@ -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, -}) diff --git a/ui/src/shared/components/RefreshingView.tsx b/ui/src/shared/components/RefreshingView.tsx index 6719257bdc..be35f55806 100644 --- a/ui/src/shared/components/RefreshingView.tsx +++ b/ui/src/shared/components/RefreshingView.tsx @@ -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 { } } -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 = {} diff --git a/ui/src/shared/components/index_views/IndexListRow.tsx b/ui/src/shared/components/index_views/IndexListRow.tsx index 291cfcd1a7..dfbfdc2cee 100644 --- a/ui/src/shared/components/index_views/IndexListRow.tsx +++ b/ui/src/shared/components/index_views/IndexListRow.tsx @@ -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 { } 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, + }) } } diff --git a/ui/src/shared/containers/GetSources.test.tsx b/ui/src/shared/containers/GetSources.test.tsx index bd27ddafa6..81eb5cd7c3 100644 --- a/ui/src/shared/containers/GetSources.test.tsx +++ b/ui/src/shared/containers/GetSources.test.tsx @@ -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: { diff --git a/ui/src/shared/containers/GetSources.tsx b/ui/src/shared/containers/GetSources.tsx index b09bfa7927..4758e2c61b 100644 --- a/ui/src/shared/containers/GetSources.tsx +++ b/ui/src/shared/containers/GetSources.tsx @@ -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 - getSources: typeof getSourcesAsync + onReadSources: typeof readSources } interface State { @@ -27,7 +27,8 @@ export class GetSources extends PureComponent { } 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 { } const mdtp = { - getSources: getSourcesAsync, + onReadSources: readSources, } export default connect( diff --git a/ui/src/shared/containers/SetActiveSource.tsx b/ui/src/shared/containers/SetActiveSource.tsx new file mode 100644 index 0000000000..577cc9fce6 --- /dev/null +++ b/ui/src/shared/containers/SetActiveSource.tsx @@ -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 +} + +interface ConnectStateProps { + activeSourceID: string + sources: Source[] + source: Source +} + +interface ConnectDispatchProps { + onSetActiveSource: typeof setActiveSource +} + +type Props = ConnectStateProps & ConnectDispatchProps & PassedInProps + +class SetActiveSource extends PureComponent { + 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( + mstp, + mdtp +)(SetActiveSource) diff --git a/ui/src/shared/containers/SetSource.tsx b/ui/src/shared/containers/SetSource.tsx deleted file mode 100644 index 5a1c84138f..0000000000 --- a/ui/src/shared/containers/SetSource.tsx +++ /dev/null @@ -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 - 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 { - 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( - mstp, - mdtp -)(withRouter(SetSource)) diff --git a/ui/src/shared/copy/notifications.ts b/ui/src/shared/copy/notifications.ts index e9c3274628..e42d5369a8 100644 --- a/ui/src/shared/copy/notifications.ts +++ b/ui/src/shared/copy/notifications.ts @@ -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 => ({ diff --git a/ui/src/shared/reducers/v2/source.ts b/ui/src/shared/reducers/v2/source.ts deleted file mode 100644 index b90187042e..0000000000 --- a/ui/src/shared/reducers/v2/source.ts +++ /dev/null @@ -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 - } -} diff --git a/ui/src/shared/utils/queryParams.ts b/ui/src/shared/utils/queryParams.ts new file mode 100644 index 0000000000..88b2b2b4c3 --- /dev/null +++ b/ui/src/shared/utils/queryParams.ts @@ -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}`) +} diff --git a/ui/src/sources/actions/index.ts b/ui/src/sources/actions/index.ts new file mode 100644 index 0000000000..1d14fac6e1 --- /dev/null +++ b/ui/src/sources/actions/index.ts @@ -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, + getState: GetState +) => { + const sourcesLink = getState().links.sources + const sources = await readSourcesAJAX(sourcesLink) + + dispatch(setSources(sources)) +} + +export const createSource = (attrs: Partial) => async ( + dispatch: Dispatch, + getState: GetState +) => { + const sourcesLink = getState().links.sources + const source = await createSourceAJAX(sourcesLink, attrs) + + dispatch(setSource(source)) +} + +export const updateSource = (source: Source) => async ( + dispatch: Dispatch +) => { + const updatedSource = await updateSourceAJAX(source) + + dispatch(setSource(updatedSource)) +} + +export const deleteSource = (sourceID: string) => async ( + dispatch: Dispatch, + 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)) +} diff --git a/ui/src/sources/apis/index.ts b/ui/src/sources/apis/index.ts index 9c528aa71e..d947786389 100644 --- a/ui/src/sources/apis/index.ts +++ b/ui/src/sources/apis/index.ts @@ -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 => { + const {data} = await AJAX({url}) + + return data.sources +} + +export const readSource = async (url: string): Promise => { + const {data: source} = await AJAX({ + url, + }) + + return source +} + +export const createSource = async ( + url: string, + attributes: Partial +): Promise => { + const {data: source} = await AJAX({ + url, + method: 'POST', + data: attributes, + }) + + return source +} + +export const updateSource = async ( + newSource: Partial +): Promise => { + 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 => { try { await AJAX({url}) } catch (error) { diff --git a/ui/src/sources/apis/v2/index.ts b/ui/src/sources/apis/v2/index.ts deleted file mode 100644 index fb36456d80..0000000000 --- a/ui/src/sources/apis/v2/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import AJAX from 'src/utils/ajax' -import {Source} from 'src/types/v2' - -export const getSources = async (): Promise => { - try { - const {data} = await AJAX({ - url: '/api/v2/sources', - }) - - return data.sources - } catch (error) { - throw error - } -} - -export const getSource = async (url: string): Promise => { - try { - const {data: source} = await AJAX({ - url, - }) - - return source - } catch (error) { - throw error - } -} - -export const createSource = async ( - url: string, - attributes: Partial -): Promise => { - try { - const {data: source} = await AJAX({ - url, - method: 'POST', - data: attributes, - }) - - return source - } catch (error) { - throw error - } -} - -export const updateSource = async ( - newSource: Partial -): Promise => { - 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 => { - try { - await AJAX({url}) - } catch (error) { - console.error(`Unable to contact source ${url}`, error) - throw error - } -} diff --git a/ui/src/sources/components/ConnectionLink.tsx b/ui/src/sources/components/ConnectionLink.tsx deleted file mode 100644 index d7ffd31a05..0000000000 --- a/ui/src/sources/components/ConnectionLink.tsx +++ /dev/null @@ -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 { - public render() { - const {source} = this.props - return ( -
- - {source.name} - {this.default} - -
- ) - } - - 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 diff --git a/ui/src/sources/components/CreateSourceOverlay.scss b/ui/src/sources/components/CreateSourceOverlay.scss new file mode 100644 index 0000000000..8e51082da1 --- /dev/null +++ b/ui/src/sources/components/CreateSourceOverlay.scss @@ -0,0 +1,11 @@ +.create-source-overlay--heading-buttons { + button { + margin-left: 5px; + } +} + +.create-source-overlay { + .form--element { + margin-bottom: 15px; + } +} diff --git a/ui/src/sources/components/CreateSourceOverlay.tsx b/ui/src/sources/components/CreateSourceOverlay.tsx new file mode 100644 index 0000000000..ea5dd39dfe --- /dev/null +++ b/ui/src/sources/components/CreateSourceOverlay.tsx @@ -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 + creationStatus: RemoteDataState +} + +class CreateSourceOverlay extends PureComponent { + public state: State = { + draftSource: { + name: '', + type: 'v1', + url: '', + }, + creationStatus: RemoteDataState.NotStarted, + } + + public render() { + const {onHide} = this.props + const {draftSource} = this.state + + return ( +
+ + +
+
+
+ +
+ + + + + + + + + + v1 + + + v2 + + + +
+
+
+
+ ) + } + + 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) => { + 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) diff --git a/ui/src/sources/components/DeleteSourceButton.tsx b/ui/src/sources/components/DeleteSourceButton.tsx new file mode 100644 index 0000000000..26245aeaa3 --- /dev/null +++ b/ui/src/sources/components/DeleteSourceButton.tsx @@ -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 +} + +interface State { + status: RemoteDataState +} + +class DeleteSourceButton extends PureComponent { + public state: State = { + status: RemoteDataState.NotStarted, + } + + public render() { + const {status} = this.state + + const buttonStatus = + status === RemoteDataState.Loading + ? ComponentStatus.Loading + : ComponentStatus.Default + + return ( + - -
- - - - ) - } - - 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 diff --git a/ui/src/sources/components/SourcesList.scss b/ui/src/sources/components/SourcesList.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/src/sources/components/SourcesList.tsx b/ui/src/sources/components/SourcesList.tsx new file mode 100644 index 0000000000..937da3d83a --- /dev/null +++ b/ui/src/sources/components/SourcesList.tsx @@ -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 => { + const rows = props.sources.map(source => ( + + )) + + return ( +
+ + + + + + + + + } columnCount={4}> + {rows} + + +
+ ) +} + +const mstp = (state: AppState) => { + const sources = getSources(state) + + return {sources} +} + +export default connect( + mstp, + null +)(SourcesList) diff --git a/ui/src/sources/components/SourcesListRow.scss b/ui/src/sources/components/SourcesListRow.scss new file mode 100644 index 0000000000..d1684772ac --- /dev/null +++ b/ui/src/sources/components/SourcesListRow.scss @@ -0,0 +1,3 @@ +.sources-list-row--connect-btn { + width: 80px; +} diff --git a/ui/src/sources/components/SourcesListRow.tsx b/ui/src/sources/components/SourcesListRow.tsx new file mode 100644 index 0000000000..302a260ed8 --- /dev/null +++ b/ui/src/sources/components/SourcesListRow.tsx @@ -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 +} + +interface OwnProps { + source: Source +} + +type Props = StateProps & DispatchProps & OwnProps + +const SourcesListRow: SFC = ({ + 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 ( + + +