Merge pull request #1631 from influxdata/dataLoader/streaming-step

feat(ui/DataLoaders): Streaming/Listening step
pull/10616/head
Iris Scholten 2018-11-29 16:40:55 -08:00 committed by GitHub
commit 2bd3031383
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1296 additions and 183 deletions

View File

@ -3535,7 +3535,7 @@ components:
links:
readOnly: true
$ref: "#/components/schemas/Links"
authorizations:
auths:
type: array
items:
$ref: "#/components/schemas/Authorization"

View File

@ -159,7 +159,7 @@ export interface Authorizations {
* @type {Array<Authorization>}
* @memberof Authorizations
*/
authorizations?: Array<Authorization>;
auths?: Array<Authorization>;
/**
*
* @type {Links}

View File

@ -70,3 +70,44 @@
color: $g11-sidewalk;
@include no-user-select();
}
.wizard-step--body, .wizard-step--body-streaming{
background-color: $g5-pepper;
color: $g20-white;
padding: 25px;
margin-left: 60px;
margin-right: 60px;
margin-top: 30px;
text-align: left;
border-radius: $radius;
> h6 {
color: $g15-platinum;
}
> p {
color: $g11-sidewalk;
padding-bottom: 10px;
}
}
.wizard-step--body-streaming {
text-align: center;
.loading{
color: $c-star
}
.success{
color: $c-rainforest
}
.error{
color: $c-fire
}
}
.wizard-step--body-snippet{
background-color: $g3-castle;
border-radius: $radius;
margin: $ix-marg-a 0;
padding: $ix-marg-b;
font-family: "RobotoMono", monospace;
}

View File

@ -0,0 +1,36 @@
// Types
import {DataSource, DataSourceType} from 'src/types/v2/dataSources'
export type Action = SetDataLoadersType | AddDataSource | RemoveDataSource
interface SetDataLoadersType {
type: 'SET_DATA_LOADERS_TYPE'
payload: {type: DataSourceType}
}
export const setDataLoadersType = (
type: DataSourceType
): SetDataLoadersType => ({
type: 'SET_DATA_LOADERS_TYPE',
payload: {type},
})
interface AddDataSource {
type: 'ADD_DATA_SOURCE'
payload: {dataSource: DataSource}
}
export const addDataSource = (dataSource: DataSource): AddDataSource => ({
type: 'ADD_DATA_SOURCE',
payload: {dataSource},
})
interface RemoveDataSource {
type: 'REMOVE_DATA_SOURCE'
payload: {dataSource: string}
}
export const removeDataSource = (dataSource: string): RemoveDataSource => ({
type: 'REMOVE_DATA_SOURCE',
payload: {dataSource},
})

View File

@ -1,50 +0,0 @@
// Types
import {DataSource} from 'src/types/v2/dataSources'
export type Action =
| AddDataSource
| RemoveDataSource
| SetDataSources
| SetActiveDataSource
interface AddDataSource {
type: 'ADD_DATA_SOURCE'
payload: {dataSource: DataSource}
}
export const addDataSource = (dataSource: DataSource): AddDataSource => ({
type: 'ADD_DATA_SOURCE',
payload: {dataSource},
})
interface RemoveDataSource {
type: 'REMOVE_DATA_SOURCE'
payload: {dataSource: string}
}
export const removeDataSource = (dataSource: string): RemoveDataSource => ({
type: 'REMOVE_DATA_SOURCE',
payload: {dataSource},
})
interface SetDataSources {
type: 'SET_DATA_SOURCES'
payload: {dataSources: DataSource[]}
}
export const setDataSources = (dataSources: DataSource[]): SetDataSources => ({
type: 'SET_DATA_SOURCES',
payload: {dataSources},
})
interface SetActiveDataSource {
type: 'SET_ACTIVE_DATA_SOURCE'
payload: {dataSource: string}
}
export const setActiveDataSource = (
dataSource: string
): SetActiveDataSource => ({
type: 'SET_ACTIVE_DATA_SOURCE',
payload: {dataSource},
})

View File

@ -2,8 +2,10 @@ import _ from 'lodash'
import AJAX from 'src/utils/ajax'
import {telegrafsAPI, authorizationsAPI} from 'src/utils/api'
import {Telegraf, TelegrafRequest, TelegrafRequestPlugins} from 'src/api'
import {telegrafsApi} from 'src/utils/api'
import {getDeep} from 'src/utils/wrappers'
export const getSetupStatus = async (url: string): Promise<boolean> => {
try {
@ -28,7 +30,7 @@ export const getTelegrafConfigTOML = async (
},
}
const response = await telegrafsApi.telegrafsTelegrafIDGet(
const response = await telegrafsAPI.telegrafsTelegrafIDGet(
telegrafID,
options
)
@ -49,7 +51,7 @@ export const createTelegrafConfig = async (): Promise<Telegraf> => {
},
],
}
const {data} = await telegrafsApi.telegrafsPost('123', telegrafRequest)
const {data} = await telegrafsAPI.telegrafsPost('123', telegrafRequest)
return data
}
@ -108,3 +110,24 @@ export const trySources = async (url: string): Promise<boolean> => {
return false
}
}
export const getTelegrafConfigs = async (org: string): Promise<Telegraf[]> => {
try {
const data = await telegrafsAPI.telegrafsGet(org)
return getDeep<Telegraf[]>(data, 'data.configurations', [])
} catch (error) {
console.error(error)
}
}
export const getAuthorizationToken = async (
username: string
): Promise<string> => {
try {
const data = await authorizationsAPI.authorizationsGet(undefined, username)
return getDeep<string>(data, 'data.auths.0.token', '')
} catch (error) {
console.error(error)
}
}

View File

@ -0,0 +1,12 @@
import {telegrafConfigsResponse, authResponse} from 'src/onboarding/resources'
const telegrafsGet = jest.fn(() => Promise.resolve(telegrafConfigsResponse))
const authorizationsGet = jest.fn(() => Promise.resolve(authResponse))
export const telegrafsAPI = {
telegrafsGet,
}
export const authorizationsAPI = {
authorizationsGet,
}

View File

@ -1,7 +1,18 @@
import {getSetupStatus, setSetupParams, SetupParams} from 'src/onboarding/apis'
import {
getSetupStatus,
setSetupParams,
SetupParams,
getTelegrafConfigs,
getAuthorizationToken,
} from 'src/onboarding/apis'
import AJAX from 'src/utils/ajax'
import {telegrafConfig, token} from 'src/onboarding/resources'
import {telegrafsAPI, authorizationsAPI} from 'src/onboarding/apis/mocks'
jest.mock('src/utils/ajax', () => require('mocks/utils/ajax'))
jest.mock('src/utils/api', () => require('src/onboarding/apis/mocks'))
describe('Onboarding.Apis', () => {
afterEach(() => {
@ -36,4 +47,27 @@ describe('Onboarding.Apis', () => {
})
})
})
describe('getTelegrafConfigs', () => {
it('should return an array of configs', async () => {
const org = 'default'
const result = await getTelegrafConfigs(org)
expect(result).toEqual([telegrafConfig])
expect(telegrafsAPI.telegrafsGet).toBeCalledWith(org)
})
})
describe('getAuthorizationToken', () => {
it('should return a token', async () => {
const username = 'iris'
const result = await getAuthorizationToken(username)
expect(result).toEqual(token)
expect(authorizationsAPI.authorizationsGet).toBeCalledWith(
undefined,
username
)
})
})
})

View File

@ -0,0 +1,95 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
Button,
ComponentColor,
ComponentSize,
ComponentStatus,
} from 'src/clockface'
import ConfigureDataSourceSwitcher from 'src/onboarding/components/ConfigureDataSourceSwitcher'
// Types
import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard'
import {DataSource, DataSourceType} from 'src/types/v2/dataSources'
export interface Props extends OnboardingStepProps {
dataSources: DataSource[]
type: DataSourceType
}
interface State {
currentDataSourceIndex: number
}
@ErrorHandling
class ConfigureDataSourceStep extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
currentDataSourceIndex: 0,
}
}
public render() {
const {setupParams, dataSources, notify} = this.props
return (
<div className="onboarding-step">
<ConfigureDataSourceSwitcher
dataSources={dataSources}
currentIndex={this.state.currentDataSourceIndex}
org={_.get(setupParams, 'org', '')}
username={_.get(setupParams, 'username', '')}
bucket={_.get(setupParams, 'bucket', '')}
notify={notify}
/>
<div className="wizard-button-bar">
<Button
color={ComponentColor.Default}
text="Back"
size={ComponentSize.Medium}
onClick={this.handlePrevious}
/>
<Button
color={ComponentColor.Primary}
text="Next"
size={ComponentSize.Medium}
onClick={this.handleNext}
status={ComponentStatus.Default}
titleText={'Next'}
/>
</div>
</div>
)
}
private handleNext = () => {
const {onIncrementCurrentStepIndex, dataSources} = this.props
const {currentDataSourceIndex} = this.state
if (currentDataSourceIndex >= dataSources.length) {
onIncrementCurrentStepIndex()
} else {
this.setState({currentDataSourceIndex: currentDataSourceIndex + 1})
}
}
private handlePrevious = () => {
const {onDecrementCurrentStepIndex} = this.props
const {currentDataSourceIndex} = this.state
if (currentDataSourceIndex === 0) {
onDecrementCurrentStepIndex()
} else {
this.setState({currentDataSourceIndex: currentDataSourceIndex - 1})
}
}
}
export default ConfigureDataSourceStep

View File

@ -10,28 +10,55 @@ import {getTelegrafConfigTOML, createTelegrafConfig} from 'src/onboarding/apis'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Button} from 'src/clockface'
import DataStreaming from 'src/onboarding/components/DataStreaming'
// Constants
import {getTelegrafConfigFailed} from 'src/shared/copy/v2/notifications'
// Types
import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard'
import {DataSource} from 'src/types/v2/dataSources'
import {NotificationAction} from 'src/types'
export interface Props extends OnboardingStepProps {
dataSource: string
export interface Props {
dataSources: DataSource[]
currentIndex: number
org: string
username: string
bucket: string
notify: NotificationAction
}
@ErrorHandling
class ConfigureDataSourceSwitcher extends PureComponent<Props> {
public render() {
const {dataSource} = this.props
const {org, bucket, username} = this.props
return (
<div>
{dataSource}
<Button text="Click to Download Config" onClick={this.handleDownload} />
</div>
)
switch (this.configurationStep) {
case 'Listening':
return <DataStreaming org={org} username={username} bucket={bucket} />
case 'CSV':
case 'Line Protocol':
default:
return (
<div>
{this.configurationStep}
<Button
text="Click to Download Config"
onClick={this.handleDownload}
/>
</div>
)
}
}
private get configurationStep() {
const {currentIndex, dataSources} = this.props
if (currentIndex === dataSources.length) {
return 'Listening'
}
return dataSources[currentIndex].name
}
private handleDownload = async () => {

View File

@ -0,0 +1,47 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import ConnectionInformation from 'src/onboarding/components/ConnectionInformation'
// Types
import {RemoteDataState} from 'src/types'
const setup = (override = {}) => {
const props = {
loading: RemoteDataState.NotStarted,
bucket: 'defbuck',
...override,
}
const wrapper = shallow(<ConnectionInformation {...props} />)
return {wrapper}
}
describe('Onboarding.Components.ConnectionInformation', () => {
it('renders', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
it('matches snapshot if loading', () => {
const {wrapper} = setup({loading: RemoteDataState.Loading})
expect(wrapper).toMatchSnapshot()
})
it('matches snapshot if success', () => {
const {wrapper} = setup({loading: RemoteDataState.Done})
expect(wrapper).toMatchSnapshot()
})
it('matches snapshot if error', () => {
const {wrapper} = setup({loading: RemoteDataState.Error})
expect(wrapper).toMatchSnapshot()
})
})

View File

@ -0,0 +1,61 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Decorator
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {RemoteDataState} from 'src/types'
export interface Props {
loading: RemoteDataState
bucket: string
}
@ErrorHandling
class ListeningResults extends PureComponent<Props> {
public render() {
return (
<>
<h4 className={this.className}>{this.header}</h4>
<p>{this.additionalText}</p>
</>
)
}
private get className(): string {
switch (this.props.loading) {
case RemoteDataState.Loading:
return 'loading'
case RemoteDataState.Done:
return 'success'
case RemoteDataState.Error:
return 'error'
}
}
private get header(): string {
switch (this.props.loading) {
case RemoteDataState.Loading:
return 'Awaiting Connection...'
case RemoteDataState.Done:
return 'Connection Found!'
case RemoteDataState.Error:
return 'Connection Not Found'
}
}
private get additionalText(): string {
switch (this.props.loading) {
case RemoteDataState.Loading:
return 'Timeout in 60 seconds'
case RemoteDataState.Done:
return `${this.props.bucket} is recieving data load and clear!`
case RemoteDataState.Error:
return 'Check config and try again'
}
}
}
export default ListeningResults

View File

@ -0,0 +1,43 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import DataListening from 'src/onboarding/components/DataListening'
import ConnectionInformation from 'src/onboarding/components/ConnectionInformation'
import {Button} from 'src/clockface'
const setup = (override = {}) => {
const props = {
bucket: 'defbuck',
...override,
}
const wrapper = shallow(<DataListening {...props} />)
return {wrapper}
}
describe('Onboarding.Components.DataListening', () => {
it('renders', () => {
const {wrapper} = setup()
const button = wrapper.find(Button)
expect(wrapper.exists()).toBe(true)
expect(button.exists()).toBe(true)
})
describe('if button is clicked', () => {
it('displays connection information', () => {
const {wrapper} = setup()
const button = wrapper.find(Button)
button.simulate('click')
const connectionInfo = wrapper.find(ConnectionInformation)
expect(wrapper.exists()).toBe(true)
expect(connectionInfo.exists()).toBe(true)
})
})
})

View File

@ -0,0 +1,134 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Apis
import {executeQuery} from 'src/shared/apis/v2/query'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
Button,
ComponentColor,
ComponentSize,
ComponentStatus,
} from 'src/clockface'
import ConnectionInformation from 'src/onboarding/components/ConnectionInformation'
// types
import {RemoteDataState} from 'src/types'
import {InfluxLanguage} from 'src/types/v2/dashboards'
export interface Props {
bucket: string
}
interface State {
loading: RemoteDataState
}
const MINUTE = 60000
const WAIT = 5000
@ErrorHandling
class DataListening extends PureComponent<Props, State> {
private intervalID: NodeJS.Timer
private startTime: number
constructor(props: Props) {
super(props)
this.state = {loading: RemoteDataState.NotStarted}
}
public componentWillUnmount() {
clearInterval(this.intervalID)
}
public render() {
return (
<div className="wizard-step--body-streaming">
{this.connectionInfo}
{this.listenButton}
</div>
)
}
private get connectionInfo(): JSX.Element {
const {loading} = this.state
if (loading === RemoteDataState.NotStarted) {
return
}
return (
<ConnectionInformation
loading={this.state.loading}
bucket={this.props.bucket}
/>
)
}
private get listenButton(): JSX.Element {
const {loading} = this.state
if (
loading === RemoteDataState.Loading ||
loading === RemoteDataState.Done
) {
return
}
return (
<Button
color={ComponentColor.Primary}
text="Listen for Data"
size={ComponentSize.Medium}
onClick={this.handleClick}
status={ComponentStatus.Default}
titleText={'Listen for Data'}
/>
)
}
private handleClick = (): void => {
this.setState({loading: RemoteDataState.Loading})
this.startTime = Number(new Date())
this.checkForData()
}
private checkForData = async (): Promise<void> => {
const {bucket} = this.props
const script = `from(bucket: "${bucket}")
|> range(start: -1m)`
let rowCount
let timePassed
try {
const response = await executeQuery(
'/api/v2/query',
script,
InfluxLanguage.Flux
)
rowCount = response.rowCount
timePassed = Number(new Date()) - this.startTime
} catch (err) {
this.setState({loading: RemoteDataState.Error})
return
}
if (rowCount > 1) {
this.setState({loading: RemoteDataState.Done})
return
}
if (timePassed >= MINUTE) {
this.setState({loading: RemoteDataState.Error})
return
}
this.intervalID = setTimeout(this.checkForData, WAIT)
}
}
export default DataListening

View File

@ -7,19 +7,21 @@ import CardSelectCard from 'src/clockface/components/card_select/CardSelectCard'
import GridSizer from 'src/clockface/components/grid_sizer/GridSizer'
// Types
import {DataSource} from 'src/types/v2/dataSources'
import {StreamingOptions} from 'src/onboarding/components/SelectDataSourceStep'
import {DataSourceType} from 'src/types/v2/dataSources'
export interface Props {
dataSources: DataSource[]
onSelectDataSource: (dataSource: string) => void
streaming: StreamingOptions
type: DataSourceType
}
const DATA_SOURCES_OPTIONS = ['CSV', 'Streaming', 'Line Protocol']
const DATA_SOURCES_OPTIONS = [
DataSourceType.CSV,
DataSourceType.Streaming,
DataSourceType.LineProtocol,
]
@ErrorHandling
class DataSourceSelector extends PureComponent<Props> {
class DataSourceTypeSelector extends PureComponent<Props> {
public render() {
return (
<GridSizer>
@ -31,7 +33,7 @@ class DataSourceSelector extends PureComponent<Props> {
name={ds}
label={ds}
checked={this.isCardChecked(ds)}
onClick={this.handleToggle(ds)}
onClick={this.handleClick(ds)}
/>
)
})}
@ -39,21 +41,15 @@ class DataSourceSelector extends PureComponent<Props> {
)
}
private isCardChecked(dataSource: string) {
const {dataSources, streaming} = this.props
if (dataSource === 'Streaming') {
return streaming === StreamingOptions.Selected
}
private isCardChecked(dataSource: DataSourceType) {
const {type} = this.props
if (dataSources.find(ds => ds.name === dataSource)) {
return true
}
return false
return dataSource === type
}
private handleToggle = (dataSource: string) => () => {
private handleClick = (dataSource: string) => () => {
this.props.onSelectDataSource(dataSource)
}
}
export default DataSourceSelector
export default DataSourceTypeSelector

View File

@ -0,0 +1,47 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import TelegrafInstructions from 'src/onboarding/components/TelegrafInstructions'
import FetchConfigID from 'src/onboarding/components/FetchConfigID'
import FetchAuthToken from 'src/onboarding/components/FetchAuthToken'
import DataListening from 'src/onboarding/components/DataListening'
// Decorator
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
bucket: string
org: string
username: string
}
@ErrorHandling
class DataStreaming extends PureComponent<Props> {
public render() {
return (
<>
<FetchConfigID org={this.props.org}>
{configID => (
<FetchAuthToken
bucket={this.props.bucket}
username={this.props.username}
>
{authToken => (
<TelegrafInstructions
authToken={authToken}
configID={configID}
/>
)}
</FetchAuthToken>
)}
</FetchConfigID>
<DataListening bucket={this.props.bucket} />
</>
)
}
}
export default DataStreaming

View File

@ -0,0 +1,28 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import FetchAuthToken from 'src/onboarding/components/FetchAuthToken'
jest.mock('src/utils/api', () => require('src/onboarding/apis/mocks'))
const setup = async (override = {}) => {
const props = {
bucket: '',
username: '',
children: jest.fn(),
...override,
}
const wrapper = await shallow(<FetchAuthToken {...props} />)
return {wrapper}
}
describe('FetchAuthToken', () => {
it('renders', async () => {
const {wrapper} = await setup()
expect(wrapper.exists()).toBe(true)
})
})

View File

@ -0,0 +1,51 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {Spinner} from 'src/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Apis
import {getAuthorizationToken} from 'src/onboarding/apis/index'
// types
import {RemoteDataState} from 'src/types'
export interface Props {
bucket: string
username: string
children: (authToken: string) => JSX.Element
}
interface State {
loading: RemoteDataState
authToken?: string
}
@ErrorHandling
class FetchAuthToken extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {loading: RemoteDataState.NotStarted}
}
public async componentDidMount() {
const {username} = this.props
this.setState({loading: RemoteDataState.Loading})
const authToken = await getAuthorizationToken(username)
this.setState({authToken, loading: RemoteDataState.Done})
}
public render() {
return (
<Spinner loading={this.state.loading}>
{this.props.children(this.state.authToken)}
</Spinner>
)
}
}
export default FetchAuthToken

View File

@ -0,0 +1,27 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import FetchConfigID from 'src/onboarding/components/FetchConfigID'
jest.mock('src/utils/api', () => require('src/onboarding/apis/mocks'))
const setup = async (override = {}) => {
const props = {
org: 'default',
children: jest.fn(),
...override,
}
const wrapper = await shallow(<FetchConfigID {...props} />)
return {wrapper}
}
describe('FetchConfigID', () => {
it('renders', async () => {
const {wrapper} = await setup()
expect(wrapper.exists()).toBe(true)
})
})

View File

@ -0,0 +1,51 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {Spinner} from 'src/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Apis
import {getTelegrafConfigs} from 'src/onboarding/apis/index'
// types
import {RemoteDataState} from 'src/types'
export interface Props {
org: string
children: (configID: string) => JSX.Element
}
interface State {
loading: RemoteDataState
configID?: string
}
@ErrorHandling
class FetchConfigID extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {loading: RemoteDataState.NotStarted}
}
public async componentDidMount() {
const {org} = this.props
this.setState({loading: RemoteDataState.Loading})
const telegrafConfigs = await getTelegrafConfigs(org)
const configID = _.get(telegrafConfigs, '0.id', '')
this.setState({configID, loading: RemoteDataState.Done})
}
public render() {
return (
<Spinner loading={this.state.loading}>
{this.props.children(this.state.configID)}
</Spinner>
)
}
}
export default FetchConfigID

View File

@ -6,7 +6,7 @@ import _ from 'lodash'
import InitStep from 'src/onboarding/components/InitStep'
import AdminStep from 'src/onboarding/components/AdminStep'
import SelectDataSourceStep from 'src/onboarding/components/SelectDataSourceStep'
import ConfigureDataSourceSwitcher from 'src/onboarding/components/ConfigureDataSourceSwitcher'
import ConfigureDataSourceStep from 'src/onboarding/components/ConfigureDataSourceStep'
import CompletionStep from 'src/onboarding/components/CompletionStep'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -14,21 +14,21 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
import {
addDataSource,
removeDataSource,
setDataSources,
} from 'src/onboarding/actions/dataSources'
setDataLoadersType,
} from 'src/onboarding/actions/dataLoaders'
// Types
import {SetupParams} from 'src/onboarding/apis'
import {DataSource} from 'src/types/v2/dataSources'
import {DataSource, DataSourceType} from 'src/types/v2/dataSources'
import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard'
interface Props {
onboardingStepProps: OnboardingStepProps
onAddDataSource: typeof addDataSource
onRemoveDataSource: typeof removeDataSource
onSetDataSources: typeof setDataSources
onSetDataLoadersType: typeof setDataLoadersType
setupParams: SetupParams
dataSources: DataSource[]
dataLoaders: {dataSources: DataSource[]; type: DataSourceType}
stepTitle: string
}
@ -39,10 +39,10 @@ class OnboardingStepSwitcher extends PureComponent<Props> {
onboardingStepProps,
stepTitle,
setupParams,
dataSources,
dataLoaders,
onSetDataLoadersType,
onAddDataSource,
onRemoveDataSource,
onSetDataSources,
} = this.props
switch (stepTitle) {
@ -54,19 +54,16 @@ class OnboardingStepSwitcher extends PureComponent<Props> {
return (
<SelectDataSourceStep
{...onboardingStepProps}
{...dataLoaders}
onSetDataLoadersType={onSetDataLoadersType}
bucket={_.get(setupParams, 'bucket', '')}
dataSources={dataSources}
onAddDataSource={onAddDataSource}
onRemoveDataSource={onRemoveDataSource}
onSetDataSources={onSetDataSources}
/>
)
case 'Configure Data Sources':
return (
<ConfigureDataSourceSwitcher
{...onboardingStepProps}
dataSource={_.get(dataSources, '0.name', '')}
/>
<ConfigureDataSourceStep {...onboardingStepProps} {...dataLoaders} />
)
case 'Complete':
return <CompletionStep {...onboardingStepProps} />

View File

@ -9,29 +9,28 @@ import {
ComponentSize,
ComponentStatus,
} from 'src/clockface'
import DataSourceSelector from 'src/onboarding/components/DataSourceSelector'
import DataSourceTypeSelector from 'src/onboarding/components/DataSourceTypeSelector'
import StreamingDataSourceSelector from 'src/onboarding/components/StreamingDataSourcesSelector'
// Types
import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard'
import {DataSource, ConfigurationState} from 'src/types/v2/dataSources'
import {
DataSource,
DataSourceType,
ConfigurationState,
} from 'src/types/v2/dataSources'
export interface Props extends OnboardingStepProps {
bucket: string
dataSources: DataSource[]
type: DataSourceType
onAddDataSource: (dataSource: DataSource) => void
onRemoveDataSource: (dataSource: string) => void
onSetDataSources: (dataSources: DataSource[]) => void
}
export enum StreamingOptions {
NotSelected = 'not selected',
Selected = 'selected',
Show = 'show',
onSetDataLoadersType: (type: DataSourceType) => void
}
interface State {
streaming: StreamingOptions
showStreamingSources: boolean
}
@ErrorHandling
@ -39,7 +38,7 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {streaming: StreamingOptions.NotSelected}
this.state = {showStreamingSources: false}
}
public render() {
@ -77,7 +76,10 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
}
private get selector(): JSX.Element {
if (this.state.streaming === StreamingOptions.Show) {
if (
this.props.type === DataSourceType.Streaming &&
this.state.showStreamingSources
) {
return (
<StreamingDataSourceSelector
dataSources={this.props.dataSources}
@ -86,17 +88,19 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
)
}
return (
<DataSourceSelector
dataSources={this.props.dataSources}
<DataSourceTypeSelector
onSelectDataSource={this.handleSelectDataSource}
streaming={this.state.streaming}
type={this.props.type}
/>
)
}
private handleClickNext = () => {
if (this.state.streaming === StreamingOptions.Selected) {
this.setState({streaming: StreamingOptions.Show})
if (
this.props.type === DataSourceType.Streaming &&
!this.state.showStreamingSources
) {
this.setState({showStreamingSources: true})
return
}
@ -104,32 +108,17 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
}
private handleClickBack = () => {
if (this.state.streaming === StreamingOptions.Show) {
this.setState({streaming: StreamingOptions.NotSelected})
if (this.props.type === DataSourceType.Streaming) {
this.setState({showStreamingSources: false})
return
}
this.props.onDecrementCurrentStepIndex()
}
private handleSelectDataSource = (dataSource: string) => {
switch (dataSource) {
case 'Streaming':
this.setState({streaming: StreamingOptions.Selected})
this.props.onSetDataSources([])
break
default:
this.setState({streaming: StreamingOptions.NotSelected})
this.props.onSetDataSources([
{
name: dataSource,
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
},
])
break
}
private handleSelectDataSource = (dataSource: DataSourceType) => {
this.props.onSetDataLoadersType(dataSource)
return
}
private handleToggleDataSource = (

View File

@ -0,0 +1,28 @@
import React from 'react'
import {shallow} from 'enzyme'
import TelegrafInstructions from 'src/onboarding/components/TelegrafInstructions'
let wrapper
const setup = (override = {}) => {
const props = {
authToken: '',
configID: '',
...override,
}
return shallow(<TelegrafInstructions {...props} />)
}
describe('TelegrafInstructions', () => {
it('renders', async () => {
const wrapper = await setup()
expect(wrapper.exists()).toBe(true)
})
it('matches snapshot', () => {
wrapper = setup()
expect(wrapper).toMatchSnapshot()
})
})

View File

@ -0,0 +1,45 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Decorator
import {ErrorHandling} from 'src/shared/decorators/errors'
export interface Props {
authToken: string
configID: string
}
@ErrorHandling
class TelegrafInstructions extends PureComponent<Props> {
public render() {
return (
<>
<h3 className="wizard-step--title">Listen for Streaming Data</h3>
<h5 className="wizard-step--sub-title">
You have selected streaming data sources. Follow the instructions
below to begin listening for incoming data.
</h5>
<div className="wizard-step--body">
<h6>Install</h6>
<p>
You can download the binaries directly from the downloads page or
from the releases section.{' '}
</p>
<h6>Start Data Stream</h6>
<p>
After installing the telegraf client, save this environment
variable. run the following command.
</p>
<p className="wizard-step--body-snippet">export INFLUX_TOKEN={this.props.authToken}</p>
<p>Run the following command.</p>
<p className="wizard-step--body-snippet">
telegraf -config http://localhost:9999/api/v2/telegrafs/{this.props.configID}
</p>
</div>
</>)
}
}
export default TelegrafInstructions

View File

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Onboarding.Components.ConnectionInformation matches snapshot if error 1`] = `
<Fragment>
<h4
className="error"
>
Connection Not Found
</h4>
<p>
Check config and try again
</p>
</Fragment>
`;
exports[`Onboarding.Components.ConnectionInformation matches snapshot if loading 1`] = `
<Fragment>
<h4
className="loading"
>
Awaiting Connection...
</h4>
<p>
Timeout in 60 seconds
</p>
</Fragment>
`;
exports[`Onboarding.Components.ConnectionInformation matches snapshot if success 1`] = `
<Fragment>
<h4
className="success"
>
Connection Found!
</h4>
<p>
defbuck is recieving data load and clear!
</p>
</Fragment>
`;

View File

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TelegrafInstructions matches snapshot 1`] = `
<Fragment>
<h3
className="wizard-step--title"
>
Listen for Streaming Data
</h3>
<h5
className="wizard-step--sub-title"
>
You have selected streaming data sources. Follow the instructions below to begin listening for incoming data.
</h5>
<div
className="wizard-step--body"
>
<h6>
Install
</h6>
<p>
You can download the binaries directly from the downloads page or from the releases section.
</p>
<h6>
Start Data Stream
</h6>
<p>
After installing the telegraf client, save this environment variable. run the following command.
</p>
<p
className="wizard-step--body-snippet"
>
export INFLUX_TOKEN=
</p>
<p>
Run the following command.
</p>
<p
className="wizard-step--body-snippet"
>
telegraf -config http://localhost:9999/api/v2/telegrafs/
</p>
</div>
</Fragment>
`;

View File

@ -24,10 +24,10 @@ import {
setStepStatus,
} from 'src/onboarding/actions/steps'
import {
setDataLoadersType,
addDataSource,
removeDataSource,
setDataSources,
} from 'src/onboarding/actions/dataSources'
} from 'src/onboarding/actions/dataLoaders'
// Constants
import {StepStatus} from 'src/clockface/constants/wizard'
@ -35,7 +35,7 @@ import {StepStatus} from 'src/clockface/constants/wizard'
// Types
import {Links} from 'src/types/v2/links'
import {SetupParams} from 'src/onboarding/apis'
import {DataSource} from 'src/types/v2/dataSources'
import {DataSource, DataSourceType} from 'src/types/v2/dataSources'
import {Notification, NotificationFunc} from 'src/types'
import {AppState} from 'src/types/v2'
@ -68,9 +68,14 @@ interface DispatchProps {
onDecrementCurrentStepIndex: typeof decrementCurrentStepIndex
onSetCurrentStepIndex: typeof setCurrentStepIndex
onSetStepStatus: typeof setStepStatus
onSetDataLoadersType: typeof setDataLoadersType
onAddDataSource: typeof addDataSource
onRemoveDataSource: typeof removeDataSource
onSetDataSources: typeof setDataSources
}
interface DataLoadersProps {
dataSources: DataSource[]
type: DataSourceType
}
interface StateProps {
@ -78,7 +83,7 @@ interface StateProps {
currentStepIndex: number
stepStatuses: StepStatus[]
setupParams: SetupParams
dataSources: DataSource[]
dataLoaders: DataLoadersProps
}
type Props = OwnProps & StateProps & DispatchProps & WithRouterProps
@ -97,18 +102,15 @@ class OnboardingWizard extends PureComponent<Props> {
constructor(props: Props) {
super(props)
this.state = {
dataSources: [],
}
}
public render() {
const {
currentStepIndex,
dataSources,
dataLoaders,
onSetDataLoadersType,
onRemoveDataSource,
onAddDataSource,
onSetDataSources,
setupParams,
} = this.props
const currentStepTitle = this.stepTitles[currentStepIndex]
@ -121,10 +123,10 @@ class OnboardingWizard extends PureComponent<Props> {
onboardingStepProps={this.onboardingStepProps}
stepTitle={currentStepTitle}
setupParams={setupParams}
dataSources={dataSources}
dataLoaders={dataLoaders}
onSetDataLoadersType={onSetDataLoadersType}
onAddDataSource={onAddDataSource}
onRemoveDataSource={onRemoveDataSource}
onSetDataSources={onSetDataSources}
/>
</div>
</WizardFullScreen>
@ -205,14 +207,14 @@ const mstp = ({
links,
onboarding: {
steps: {currentStepIndex, stepStatuses, setupParams},
dataSources,
dataLoaders,
},
}: AppState): StateProps => ({
links,
currentStepIndex,
stepStatuses,
setupParams,
dataSources,
dataLoaders,
})
const mdtp: DispatchProps = {
@ -222,9 +224,9 @@ const mdtp: DispatchProps = {
onIncrementCurrentStepIndex: incrementCurrentStepIndex,
onSetCurrentStepIndex: setCurrentStepIndex,
onSetStepStatus: setStepStatus,
onSetDataLoadersType: setDataLoadersType,
onAddDataSource: addDataSource,
onRemoveDataSource: removeDataSource,
onSetDataSources: setDataSources,
}
export default connect<StateProps, DispatchProps, OwnProps>(

View File

@ -0,0 +1,126 @@
// Reducer
import dataLoadersReducer, {
// DataLoadersState,
INITIAL_STATE,
} from 'src/onboarding/reducers/dataLoaders'
// Actions
import {
setDataLoadersType,
addDataSource,
removeDataSource,
} from 'src/onboarding/actions/dataLoaders'
import {DataSourceType, ConfigurationState} from 'src/types/v2/dataSources'
describe('dataLoader reducer', () => {
describe('if type is streaming', () => {
it('can set a type', () => {
const actual = dataLoadersReducer(
INITIAL_STATE,
setDataLoadersType(DataSourceType.Streaming)
)
const expected = {dataSources: [], type: DataSourceType.Streaming}
expect(actual).toEqual(expected)
})
})
describe('if type is not streaming', () => {
it('cant set a type not streaming', () => {
const actual = dataLoadersReducer(
INITIAL_STATE,
setDataLoadersType(DataSourceType.CSV)
)
const expected = {
dataSources: [
{
name: 'CSV',
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
},
],
type: DataSourceType.CSV,
}
expect(actual).toEqual(expected)
})
})
describe('if data source is added', () => {
it('can add a data source', () => {
const actual = dataLoadersReducer(
INITIAL_STATE,
addDataSource({
name: 'CSV',
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
})
)
const expected = {
dataSources: [
{
name: 'CSV',
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
},
],
type: DataSourceType.Empty,
}
expect(actual).toEqual(expected)
})
})
it('can add a streaming data source', () => {
const actual = dataLoadersReducer(
{...INITIAL_STATE, type: DataSourceType.Streaming},
addDataSource({
name: 'CPU',
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
})
)
const expected = {
dataSources: [
{
name: 'CPU',
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
},
],
type: DataSourceType.Streaming,
}
expect(actual).toEqual(expected)
})
it('can remove a streaming data source', () => {
const actual = dataLoadersReducer(
{
...INITIAL_STATE,
type: DataSourceType.Streaming,
dataSources: [
{
name: 'CPU',
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
},
],
},
removeDataSource('CPU')
)
const expected = {
dataSources: [],
type: DataSourceType.Streaming,
}
expect(actual).toEqual(expected)
})
})

View File

@ -0,0 +1,41 @@
// Utils
import {getInitialDataSources} from 'src/onboarding/utils/dataLoaders'
// Types
import {Action} from 'src/onboarding/actions/dataLoaders'
import {DataSource, DataSourceType} from 'src/types/v2/dataSources'
export interface DataLoadersState {
dataSources: DataSource[]
type: DataSourceType
}
export const INITIAL_STATE: DataLoadersState = {
dataSources: [],
type: DataSourceType.Empty,
}
export default (state = INITIAL_STATE, action: Action): DataLoadersState => {
switch (action.type) {
case 'SET_DATA_LOADERS_TYPE':
return {
...state,
type: action.payload.type,
dataSources: getInitialDataSources(action.payload.type),
}
case 'ADD_DATA_SOURCE':
return {
...state,
dataSources: [...state.dataSources, action.payload.dataSource],
}
case 'REMOVE_DATA_SOURCE':
return {
...state,
dataSources: state.dataSources.filter(
ds => ds.name !== action.payload.dataSource
),
}
default:
return state
}
}

View File

@ -1,27 +0,0 @@
// Types
import {Action} from 'src/onboarding/actions/dataSources'
import {DataSource} from 'src/types/v2/dataSources'
export type DataSourcesState = DataSource[]
const INITIAL_STATE: DataSourcesState = []
export default (state = INITIAL_STATE, action: Action): DataSourcesState => {
switch (action.type) {
case 'ADD_DATA_SOURCE':
return [...state, action.payload.dataSource]
case 'REMOVE_DATA_SOURCE':
return state.filter(ds => ds.name !== action.payload.dataSource)
case 'SET_DATA_SOURCES':
return action.payload.dataSources
case 'SET_ACTIVE_DATA_SOURCE':
return state.map(ds => {
if (ds.name === action.payload.dataSource) {
return {...ds, active: true}
}
return {...ds, active: false}
})
default:
return state
}
}

View File

@ -2,17 +2,17 @@
import {combineReducers} from 'redux'
// Reducers
import dataSourcesReducer, {
DataSourcesState,
} from 'src/onboarding/reducers/dataSources'
import dataLoadersReducer, {
DataLoadersState,
} from 'src/onboarding/reducers/dataLoaders'
import stepsReducer, {OnboardingStepsState} from 'src/onboarding/reducers/steps'
export interface OnboardingState {
steps: OnboardingStepsState
dataSources: DataSourcesState
dataLoaders: DataLoadersState
}
export default combineReducers<OnboardingState>({
steps: stepsReducer,
dataSources: dataSourcesReducer,
dataLoaders: dataLoadersReducer,
})

View File

@ -0,0 +1,97 @@
export const telegrafConfigID = '030358c935b18000'
export const telegrafConfig = {
id: telegrafConfigID,
name: 'in n out',
created: '2018-11-28T18:56:48.854337-08:00',
lastModified: '2018-11-28T18:56:48.854337-08:00',
lastModifiedBy: '030358b695318000',
agent: {collectionInterval: 15},
plugins: [
{name: 'cpu', type: 'input', comment: 'this is a test', config: {}},
{
name: 'influxdb_v2',
type: 'output',
comment: 'write to influxdb v2',
config: {
urls: ['http://127.0.0.1:9999'],
token:
'm4aUjEIhM758JzJgRmI6f3KNOBw4ZO77gdwERucF0bj4QOLHViD981UWzjaxW9AbyA5THOMBp2SVZqzbui2Ehw==',
organization: 'default',
bucket: 'defbuck',
},
},
],
}
export const telegrafConfigsResponse = {
data: {
configurations: [telegrafConfig],
},
status: 200,
statusText: 'OK',
headers: {
date: 'Thu, 29 Nov 2018 18:10:21 GMT',
'content-length': '570',
'content-type': 'application/json; charset=utf-8',
},
config: {
transformRequest: {},
transformResponse: {},
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
headers: {Accept: 'application/json, text/plain, */*'},
method: 'get',
url: '/api/v2/telegrafs?org=',
},
request: {},
}
export const token =
'm4aUjEIhM758JzJgRmI6f3KNOBw4ZO77gdwERucF0bj4QOLHViD981UWzjaxW9AbyA5THOMBp2SVZqzbui2Ehw=='
export const authResponse = {
data: {
links: {self: '/api/v2/authorizations'},
auths: [
{
links: {
self: '/api/v2/authorizations/030358b6aa718000',
user: '/api/v2/users/030358b695318000',
},
id: '030358b6aa718000',
token,
status: 'active',
user: 'iris',
userID: '030358b695318000',
permissions: [
{action: 'create', resource: 'user'},
{action: 'delete', resource: 'user'},
{action: 'write', resource: 'org'},
{action: 'write', resource: 'bucket/030358b6aa318000'},
],
},
],
},
status: 200,
statusText: 'OK',
headers: {
date: 'Thu, 29 Nov 2018 18:10:21 GMT',
'content-length': '522',
'content-type': 'application/json; charset=utf-8',
},
config: {
transformRequest: {},
transformResponse: {},
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
headers: {Accept: 'application/json, text/plain, */*'},
method: 'get',
url: '/api/v2/authorizations?user=',
},
request: {},
}

View File

@ -0,0 +1,17 @@
// Types
import {DataSourceType, ConfigurationState} from 'src/types/v2/dataSources'
export const getInitialDataSources = (type: DataSourceType) => {
if (type === DataSourceType.Streaming) {
return []
}
return [
{
name: type,
configured: ConfigurationState.Unconfigured,
active: true,
configs: null,
},
]
}

View File

@ -7,6 +7,13 @@ export enum ConfigurationState {
Error = 'error',
}
export enum DataSourceType {
CSV = 'CSV',
Streaming = 'Streaming',
LineProtocol = 'Line Protocol',
Empty = '',
}
export interface DataSource {
name: string
configured: ConfigurationState

View File

@ -4,6 +4,7 @@ import {
DashboardsApi,
CellsApi,
TelegrafsApi,
AuthorizationsApi,
} from 'src/api'
const basePath = '/api/v2'
@ -12,4 +13,5 @@ export const taskAPI = new TasksApi({basePath})
export const usersAPI = new UsersApi({basePath})
export const dashboardsAPI = new DashboardsApi({basePath})
export const cellsAPI = new CellsApi({basePath})
export const telegrafsApi = new TelegrafsApi({basePath})
export const telegrafsAPI = new TelegrafsApi({basePath})
export const authorizationsAPI = new AuthorizationsApi({basePath})