From 388265604bfadde2063f69ff2c2bd466e5b9ed39 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 12 Dec 2018 15:54:00 -0800 Subject: [PATCH 01/14] fix(ui/dataLoaders): Update the button labels and funcitonality in onboarding steps --- ui/mocks/dummyData.ts | 1 + .../onboarding/components/AdminStep.test.tsx | 27 +++ ui/src/onboarding/components/AdminStep.tsx | 4 +- .../components/CompletionStep.test.tsx | 27 +++ .../onboarding/components/CompletionStep.tsx | 2 +- .../components/OnboardingSideBar.tsx | 6 +- .../__snapshots__/AdminStep.test.tsx.snap | 158 +++++++++++++++ .../CompletionStep.test.tsx.snap | 43 +++++ .../ConfigureDataSourceStep.test.tsx | 177 +++++++++++++++++ .../configureStep/ConfigureDataSourceStep.tsx | 80 ++++++-- .../SelectDataSourceStep.test.tsx | 180 ++++++++++++++++++ .../selectionStep/SelectDataSourceStep.tsx | 78 +++++++- .../verifyStep/VerifyDataStep.test.tsx | 55 +++++- .../components/verifyStep/VerifyDataStep.tsx | 34 +++- .../containers/OnboardingWizard.tsx | 18 +- .../containers/OnboardingWizardPage.tsx | 8 +- 16 files changed, 849 insertions(+), 49 deletions(-) create mode 100644 ui/src/onboarding/components/AdminStep.test.tsx create mode 100644 ui/src/onboarding/components/CompletionStep.test.tsx create mode 100644 ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap create mode 100644 ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap create mode 100644 ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx create mode 100644 ui/src/onboarding/components/selectionStep/SelectDataSourceStep.test.tsx diff --git a/ui/mocks/dummyData.ts b/ui/mocks/dummyData.ts index 4f2bc1c8d4..65a78e248d 100644 --- a/ui/mocks/dummyData.ts +++ b/ui/mocks/dummyData.ts @@ -282,6 +282,7 @@ export const defaultOnboardingStepProps: OnboardingStepProps = { notify: jest.fn(), onCompleteSetup: jest.fn(), onExit: jest.fn(), + onSetSubstepIndex: jest.fn(), } export const token = diff --git a/ui/src/onboarding/components/AdminStep.test.tsx b/ui/src/onboarding/components/AdminStep.test.tsx new file mode 100644 index 0000000000..4a8744d781 --- /dev/null +++ b/ui/src/onboarding/components/AdminStep.test.tsx @@ -0,0 +1,27 @@ +// Libraries +import React from 'react' +import {shallow} from 'enzyme' + +// Components +import AdminStep from 'src/onboarding/components/AdminStep' + +// Dummy Data +import {defaultOnboardingStepProps} from 'mocks/dummyData' + +const setup = (override = {}) => { + const props = { + ...defaultOnboardingStepProps, + ...override, + } + + return shallow() +} + +describe('Onboarding.Components.AdminStep', () => { + it('renders', () => { + const wrapper = setup() + + expect(wrapper.exists()).toBe(true) + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/ui/src/onboarding/components/AdminStep.tsx b/ui/src/onboarding/components/AdminStep.tsx index 765db50262..0319afdeae 100644 --- a/ui/src/onboarding/components/AdminStep.tsx +++ b/ui/src/onboarding/components/AdminStep.tsx @@ -148,13 +148,13 @@ class AdminStep extends PureComponent {
+ +`; diff --git a/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap b/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap new file mode 100644 index 0000000000..4344950f9f --- /dev/null +++ b/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Onboarding.Components.CompletionStep renders 1`] = ` +
+
+

+ Setup Complete! +

+
+
+
+
+`; diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx new file mode 100644 index 0000000000..db7473dbfe --- /dev/null +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx @@ -0,0 +1,177 @@ +// Libraries +import React from 'react' +import {shallow} from 'enzyme' + +// Components +import {ConfigureDataSourceStep} from 'src/onboarding/components/configureStep/ConfigureDataSourceStep' +import ConfigureDataSourceSwitcher from 'src/onboarding/components/configureStep/ConfigureDataSourceSwitcher' +import {Button} from 'src/clockface' + +// Types +import {DataLoaderType} from 'src/types/v2/dataLoaders' + +// Dummy Data +import { + defaultOnboardingStepProps, + cpuTelegrafPlugin, + redisTelegrafPlugin, + diskTelegrafPlugin, +} from 'mocks/dummyData' + +const setup = (override = {}) => { + const props = { + ...defaultOnboardingStepProps, + telegrafPlugins: [], + onSetActiveTelegrafPlugin: jest.fn(), + onUpdateTelegrafPluginConfig: jest.fn(), + onSetPluginConfiguration: jest.fn(), + type: DataLoaderType.Empty, + onAddConfigValue: jest.fn(), + onRemoveConfigValue: jest.fn(), + onSaveTelegrafConfig: jest.fn(), + authToken: '', + params: { + stepID: '3', + substepID: '0', + }, + location: null, + router: null, + routes: [], + ...override, + } + + return shallow() +} + +describe('Onboarding.Components.ConfigureStep.ConfigureDataSourceStep', () => { + it('renders switcher and buttons', async () => { + const wrapper = setup() + const switcher = wrapper.find(ConfigureDataSourceSwitcher) + const buttons = wrapper.find(Button) + + expect(wrapper.exists()).toBe(true) + expect(switcher.exists()).toBe(true) + expect(buttons.length).toBe(3) + }) + + describe('if type is not streaming', () => { + it('renders back and next buttons with correct text', () => { + const wrapper = setup({type: DataLoaderType.LineProtocol}) + const backButton = wrapper.find('[data-test="back"]') + const nextButton = wrapper.find('[data-test="next"]') + + expect(backButton.prop('text')).toBe('Back to Select Data Source Type') + expect(nextButton.prop('text')).toBe('Continue to Verify') + }) + }) + + describe('if type is streaming', () => { + describe('if the substep is 0', () => { + it('renders back button with correct text', () => { + const wrapper = setup({ + type: DataLoaderType.Streaming, + params: {stepID: '3', substepID: '0'}, + }) + const backButton = wrapper.find('[data-test="back"]') + + expect(backButton.prop('text')).toBe('Back to Select Streaming Sources') + }) + + describe('when the back button is clicked', () => { + it('calls prop functions as expected', () => { + const onSetActiveTelegrafPlugin = jest.fn() + const onSetSubstepIndex = jest.fn() + const wrapper = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [cpuTelegrafPlugin], + params: {stepID: '3', substepID: '0'}, + onSetActiveTelegrafPlugin, + onSetSubstepIndex, + }) + const backButton = wrapper.find('[data-test="back"]') + backButton.simulate('click') + + expect(onSetSubstepIndex).toBeCalledWith(2, 'streaming') + }) + }) + }) + + describe('if its the last stubstep', () => { + it('renders the next button with correct text', () => { + const wrapper = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [cpuTelegrafPlugin], + params: {stepID: '3', substepID: '1'}, + }) + const nextButton = wrapper.find('[data-test="next"]') + + expect(nextButton.prop('text')).toBe('Continue to Verify') + }) + }) + + describe('if its the neither the last or firt stubstep', () => { + it('renders the next and back buttons with correct text', () => { + const wrapper = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [ + cpuTelegrafPlugin, + redisTelegrafPlugin, + diskTelegrafPlugin, + ], + params: {stepID: '3', substepID: '1'}, + }) + const nextButton = wrapper.find('[data-test="next"]') + const backButton = wrapper.find('[data-test="back"]') + + expect(nextButton.prop('text')).toBe('Continue to Disk') + expect(backButton.prop('text')).toBe('Back to Cpu') + }) + + describe('when the back button is clicked', () => { + it('calls prop functions as expected', () => { + const onSetActiveTelegrafPlugin = jest.fn() + const onSetSubstepIndex = jest.fn() + const wrapper = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [ + cpuTelegrafPlugin, + redisTelegrafPlugin, + diskTelegrafPlugin, + ], + params: {stepID: '3', substepID: '1'}, + onSetActiveTelegrafPlugin, + onSetSubstepIndex, + }) + const backButton = wrapper.find('[data-test="back"]') + backButton.simulate('click') + + expect(onSetActiveTelegrafPlugin).toBeCalledWith('cpu') + expect(onSetSubstepIndex).toBeCalledWith(3, 0) + }) + }) + + describe('when the next button is clicked', () => { + it('calls prop functions as expected', () => { + const onSetActiveTelegrafPlugin = jest.fn() + const onSetSubstepIndex = jest.fn() + const wrapper = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [ + cpuTelegrafPlugin, + redisTelegrafPlugin, + diskTelegrafPlugin, + ], + params: {stepID: '3', substepID: '1'}, + onSetActiveTelegrafPlugin, + onSetSubstepIndex, + }) + const nextButton = wrapper.find('[data-test="next"]') + nextButton.simulate('click') + + expect(onSetActiveTelegrafPlugin).toBeCalledWith('disk') + expect(onSetSubstepIndex).toBeCalledWith(3, 2) + }) + }) + }) + }) +}) diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx index 3bcbddafae..16a8874cee 100644 --- a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx @@ -60,7 +60,7 @@ interface RouterProps { type Props = OwnProps & WithRouterProps & RouterProps @ErrorHandling -class ConfigureDataSourceStep extends PureComponent { +export class ConfigureDataSourceStep extends PureComponent { constructor(props: Props) { super(props) } @@ -108,17 +108,19 @@ class ConfigureDataSourceStep extends PureComponent {
{this.skipLink} @@ -127,6 +129,48 @@ class ConfigureDataSourceStep extends PureComponent { ) } + private get nextButtonText(): string { + const { + telegrafPlugins, + params: {substepID}, + type, + } = this.props + + const index = +substepID + + if (type === DataLoaderType.Streaming) { + if (index + 1 > telegrafPlugins.length - 1) { + return 'Continue to Verify' + } + return `Continue to ${_.startCase( + _.get(telegrafPlugins, `${index + 1}.name`) + )}` + } + + return 'Continue to Verify' + } + + private get backButtonText(): string { + const { + telegrafPlugins, + params: {substepID}, + type, + } = this.props + + const index = +substepID + + if (type === DataLoaderType.Streaming) { + if (index < 1) { + return 'Back to Select Streaming Sources' + } + return `Back to ${_.startCase( + _.get(telegrafPlugins, `${index - 1}.name`) + )}` + } + + return 'Back to Select Data Source Type' + } + private get skipLink() { return (
{this.skipLink} @@ -96,6 +98,53 @@ class SelectDataSourceStep extends PureComponent { ) } + private get nextButtonStatus(): ComponentStatus { + const {type, telegrafPlugins} = this.props + + const isTypeEmpty = type === DataLoaderType.Empty + const isStreamingWithoutPlugin = + type === DataLoaderType.Streaming && + this.isStreaming && + !telegrafPlugins.length + + if (isTypeEmpty || isStreamingWithoutPlugin) { + return ComponentStatus.Disabled + } + + return ComponentStatus.Default + } + + private get nextButtonText(): string { + const {type, telegrafPlugins} = this.props + + switch (type) { + case DataLoaderType.CSV: + return 'Continue to CSV Configuration' + case DataLoaderType.Streaming: + if (this.isStreaming) { + if (telegrafPlugins.length) { + return `Continue to ${_.startCase(telegrafPlugins[0].name)}` + } + return 'Continue to Plugin Configuration' + } + return 'Continue to Streaming Selection' + case DataLoaderType.LineProtocol: + return 'Continue to Line Protocol Configuration' + case DataLoaderType.Empty: + return 'Continue to Configuration' + } + } + + private get backButtonText(): string { + if (this.props.type === DataLoaderType.Streaming) { + if (this.isStreaming) { + return 'Back to Data Source Selection' + } + } + + return 'Back to Admin Setup' + } + private get title(): string { const {bucket} = this.props if (this.isStreaming) { @@ -145,25 +194,38 @@ class SelectDataSourceStep extends PureComponent { private handleClickNext = () => { const { - router, params: {stepID}, telegrafPlugins, onSetActiveTelegrafPlugin, + onSetSubstepIndex, } = this.props if (this.props.type === DataLoaderType.Streaming && !this.isStreaming) { - router.push(`/onboarding/${stepID}/streaming`) + onSetSubstepIndex(+stepID, 'streaming') + onSetActiveTelegrafPlugin('') return } - const name = _.get(telegrafPlugins, '0.name', '') - onSetActiveTelegrafPlugin(name) + if (this.isStreaming) { + const name = _.get(telegrafPlugins, '0.name', '') + onSetActiveTelegrafPlugin(name) + } this.handleSetStepStatus() this.props.onIncrementCurrentStepIndex() } private handleClickBack = () => { + const { + params: {stepID}, + onSetCurrentStepIndex, + } = this.props + + if (this.isStreaming) { + onSetCurrentStepIndex(+stepID) + return + } + this.props.onDecrementCurrentStepIndex() } diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx index b00bf04030..4c2396ff97 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx @@ -11,12 +11,12 @@ import {Button} from 'src/clockface' import {DataLoaderType} from 'src/types/v2/dataLoaders' // Constants -import {defaultOnboardingStepProps} from 'mocks/dummyData' +import {defaultOnboardingStepProps, cpuTelegrafPlugin} from 'mocks/dummyData' const setup = (override = {}) => { const props = { - type: DataLoaderType.Empty, ...defaultOnboardingStepProps, + type: DataLoaderType.Empty, telegrafPlugins: [], stepIndex: 4, onSetActiveTelegrafPlugin: jest.fn(), @@ -38,4 +38,55 @@ describe('Onboarding.Components.VerifyStep.VerifyDataStep', () => { expect(buttons.length).toBe(3) expect(switcher.exists()).toBe(true) }) + + describe('if type is streaming', () => { + it('renders back button with correct text', () => { + const {wrapper} = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [cpuTelegrafPlugin], + }) + const nextButton = wrapper.find('[data-test="next"]') + const backButton = wrapper.find('[data-test="back"]') + + expect(nextButton.prop('text')).toBe('Continue to Completion') + expect(backButton.prop('text')).toBe('Back to Cpu Configuration') + }) + + describe('when the back button is clicked', () => { + describe('if the type is streaming', () => { + it('calls the prop functions as expected', () => { + const onSetSubstepIndex = jest.fn() + const onSetActiveTelegrafPlugin = jest.fn() + const {wrapper} = setup({ + type: DataLoaderType.Streaming, + telegrafPlugins: [cpuTelegrafPlugin], + onSetSubstepIndex, + onSetActiveTelegrafPlugin, + }) + const backButton = wrapper.find('[data-test="back"]') + backButton.simulate('click') + + expect(onSetSubstepIndex).toBeCalledWith(3, 0) + expect(onSetActiveTelegrafPlugin).toBeCalledWith('cpu') + }) + }) + + describe('if the type is line protocol', () => { + it('calls the prop functions as expected', () => { + const onDecrementCurrentStepIndex = jest.fn() + const onSetActiveTelegrafPlugin = jest.fn() + const {wrapper} = setup({ + type: DataLoaderType.LineProtocol, + onDecrementCurrentStepIndex, + onSetActiveTelegrafPlugin, + }) + const backButton = wrapper.find('[data-test="back"]') + backButton.simulate('click') + + expect(onDecrementCurrentStepIndex).toBeCalled() + expect(onSetActiveTelegrafPlugin).toBeCalledWith('') + }) + }) + }) + }) }) diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx index 76a5a932a2..ef41303b30 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx @@ -51,17 +51,19 @@ class VerifyDataStep extends PureComponent {
{this.skipLink} @@ -83,17 +85,37 @@ class VerifyDataStep extends PureComponent { ) } + private get backButtonText(): string { + return `Back to ${_.startCase(this.previousStepName) || ''} Configuration` + } + + private get previousStepName() { + const {telegrafPlugins, type} = this.props + + if (type === DataLoaderType.Streaming) { + return _.get(telegrafPlugins, `${telegrafPlugins.length - 1}.name`, '') + } + + return type + } + private handleDecrementStep = () => { const { telegrafPlugins, onSetActiveTelegrafPlugin, onDecrementCurrentStepIndex, + onSetSubstepIndex, + stepIndex, + type, } = this.props - const name = _.get(telegrafPlugins, `${telegrafPlugins.length - 1}.name`) - onSetActiveTelegrafPlugin(name) - - onDecrementCurrentStepIndex() + if (type === DataLoaderType.Streaming) { + onSetSubstepIndex(stepIndex - 1, telegrafPlugins.length - 1 || 0) + onSetActiveTelegrafPlugin(this.previousStepName) + } else { + onDecrementCurrentStepIndex() + onSetActiveTelegrafPlugin('') + } } private jumpToCompletionStep = () => { diff --git a/ui/src/onboarding/containers/OnboardingWizard.tsx b/ui/src/onboarding/containers/OnboardingWizard.tsx index cb8fa5ab0a..eb00780cc0 100644 --- a/ui/src/onboarding/containers/OnboardingWizard.tsx +++ b/ui/src/onboarding/containers/OnboardingWizard.tsx @@ -47,6 +47,7 @@ export interface OnboardingStepProps { onIncrementCurrentStepIndex: () => void onDecrementCurrentStepIndex: () => void onSetStepStatus: (index: number, status: StepStatus) => void + onSetSubstepIndex: (index: number, subStep: number | 'streaming') => void stepStatuses: StepStatus[] stepTitles: string[] setupParams: SetupParams @@ -64,10 +65,7 @@ interface OwnProps { onIncrementCurrentStepIndex: () => void onDecrementCurrentStepIndex: () => void onSetCurrentStepIndex: (stepNumber: number) => void - onSetCurrentSubStepIndex: ( - stepNumber: number, - substep: number | 'streaming' - ) => void + onSetSubstepIndex: (stepNumber: number, substep: number | 'streaming') => void } interface DispatchProps { @@ -208,13 +206,15 @@ class OnboardingWizard extends PureComponent { } private handleNewSourceClick = () => { - const {onSetCurrentSubStepIndex} = this.props - onSetCurrentSubStepIndex(2, 'streaming') + const {onSetSubstepIndex, onSetActiveTelegrafPlugin} = this.props + + onSetActiveTelegrafPlugin('') + onSetSubstepIndex(2, 'streaming') } private handleClickSideBarTab = (telegrafPluginID: string) => { const { - onSetCurrentSubStepIndex, + onSetSubstepIndex, onSetActiveTelegrafPlugin, dataLoaders: {telegrafPlugins}, } = this.props @@ -226,7 +226,7 @@ class OnboardingWizard extends PureComponent { 0 ) - onSetCurrentSubStepIndex(3, index) + onSetSubstepIndex(3, index) onSetActiveTelegrafPlugin(telegrafPluginID) } @@ -247,6 +247,7 @@ class OnboardingWizard extends PureComponent { onSetStepStatus, onSetSetupParams, onSetCurrentStepIndex, + onSetSubstepIndex, onDecrementCurrentStepIndex, onIncrementCurrentStepIndex, } = this.props @@ -256,6 +257,7 @@ class OnboardingWizard extends PureComponent { stepTitles: this.stepTitles, currentStepIndex, onSetCurrentStepIndex, + onSetSubstepIndex, onIncrementCurrentStepIndex, onDecrementCurrentStepIndex, onSetStepStatus, diff --git a/ui/src/onboarding/containers/OnboardingWizardPage.tsx b/ui/src/onboarding/containers/OnboardingWizardPage.tsx index c8d16cdbfa..b89d45e812 100644 --- a/ui/src/onboarding/containers/OnboardingWizardPage.tsx +++ b/ui/src/onboarding/containers/OnboardingWizardPage.tsx @@ -61,7 +61,7 @@ export class OnboardingWizardPage extends PureComponent { onDecrementCurrentStepIndex={this.handleDecrementStepIndex} onIncrementCurrentStepIndex={this.handleIncrementStepIndex} onSetCurrentStepIndex={this.setStepIndex} - onSetCurrentSubStepIndex={this.setSubstepIndex} + onSetSubstepIndex={this.setSubstepIndex} currentStepIndex={+params.stepID} onCompleteSetup={this.handleCompleteSetup} /> @@ -74,9 +74,11 @@ export class OnboardingWizardPage extends PureComponent { } private handleDecrementStepIndex = () => { - const {router} = this.props + const { + params: {stepID}, + } = this.props - router.goBack() + this.setStepIndex(+stepID - 1) } private handleIncrementStepIndex = () => { From 8a93b34198d8301e9133a0d398d123e567153b4a Mon Sep 17 00:00:00 2001 From: Palak Bhojani Date: Tue, 18 Dec 2018 11:30:34 -0800 Subject: [PATCH 02/14] Create styling for file input on streaming data sources --- ui/src/clockface/components/inputs/Input.tsx | 6 +- ui/src/clockface/styles.scss | 1 + .../__snapshots__/Settings.test.tsx.snap | 2 + .../__snapshots__/Tokens.test.tsx.snap | 2 + ui/src/onboarding/actions/dataLoaders.ts | 23 +++- .../components/OnboardingStepSwitcher.tsx | 4 + .../__snapshots__/AdminStep.test.tsx.snap | 5 + .../ConfigureDataSourceStep.test.tsx | 1 + .../configureStep/ConfigureDataSourceStep.tsx | 4 + .../ConfigureDataSourceSwitcher.tsx | 4 + .../streaming/ArrayFormElement.test.tsx | 10 +- .../streaming/ArrayFormElement.tsx | 52 +++++--- .../streaming/ConfigFieldSwitcher.test.tsx | 3 + .../streaming/ConfigFieldSwitcher.tsx | 18 ++- .../streaming/MultipleInput.scss | 11 ++ .../streaming/MultipleInput.test.tsx | 38 ++++++ .../configureStep/streaming/MultipleInput.tsx | 116 ++++++++++++++++++ .../streaming/MultipleRow.test.tsx | 33 +++++ .../configureStep/streaming/MultipleRow.tsx | 113 +++++++++++++++++ .../streaming/MultipleRows.test.tsx | 33 +++++ .../configureStep/streaming/MultipleRows.tsx | 54 ++++++++ .../streaming/PluginConfigForm.test.tsx | 2 + .../streaming/PluginConfigForm.tsx | 7 +- .../streaming/PluginConfigSwitcher.test.tsx | 3 + .../streaming/PluginConfigSwitcher.tsx | 4 + .../configureStep/streaming/Row.test.tsx | 34 +++++ .../configureStep/streaming/Row.tsx | 78 ++++++++++++ .../containers/OnboardingWizard.tsx | 5 + ui/src/onboarding/reducers/dataLoaders.ts | 22 ++++ .../shared/components/InputClickToEdit.scss | 13 ++ ui/src/shared/components/InputClickToEdit.tsx | 24 ++-- 31 files changed, 688 insertions(+), 37 deletions(-) create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleInput.scss create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleInput.test.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleRow.test.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleRow.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleRows.test.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/MultipleRows.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/Row.test.tsx create mode 100644 ui/src/onboarding/components/configureStep/streaming/Row.tsx create mode 100644 ui/src/shared/components/InputClickToEdit.scss diff --git a/ui/src/clockface/components/inputs/Input.tsx b/ui/src/clockface/components/inputs/Input.tsx index 3c556d5f4b..d2a10267ff 100644 --- a/ui/src/clockface/components/inputs/Input.tsx +++ b/ui/src/clockface/components/inputs/Input.tsx @@ -23,10 +23,11 @@ export enum AutoComplete { } interface Props { + id?: string min?: number max?: number name?: string - value?: string | number + value: string | number placeholder?: string autocomplete?: AutoComplete onChange?: (e: ChangeEvent) => void @@ -52,6 +53,7 @@ interface Props { class Input extends Component { public static defaultProps: Partial = { + id: '', name: '', value: '', placeholder: '', @@ -66,6 +68,7 @@ class Input extends Component { public render() { const { + id, min, max, name, @@ -89,6 +92,7 @@ class Input extends Component { return (
({ + type: 'SET_TELEGRAF_PLUGIN_CONFIG_VALUE', + payload: {pluginName, field, valueIndex, value}, +}) + interface SetTelegrafConfigID { type: 'SET_TELEGRAF_CONFIG_ID' payload: {id: string} diff --git a/ui/src/onboarding/components/OnboardingStepSwitcher.tsx b/ui/src/onboarding/components/OnboardingStepSwitcher.tsx index 28bde0aaf3..9b0ce768a4 100644 --- a/ui/src/onboarding/components/OnboardingStepSwitcher.tsx +++ b/ui/src/onboarding/components/OnboardingStepSwitcher.tsx @@ -23,6 +23,7 @@ import { addPluginBundleWithPlugins, removePluginBundleWithPlugins, setPluginConfiguration, + setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Types @@ -44,6 +45,7 @@ interface Props { onSaveTelegrafConfig: typeof createTelegrafConfigAsync onAddPluginBundle: typeof addPluginBundleWithPlugins onRemovePluginBundle: typeof removePluginBundleWithPlugins + onSetConfigArrayValue: typeof setConfigArrayValue } @ErrorHandling @@ -63,6 +65,7 @@ class OnboardingStepSwitcher extends PureComponent { onRemoveConfigValue, onAddPluginBundle, onRemovePluginBundle, + onSetConfigArrayValue, } = this.props switch (currentStepIndex) { @@ -99,6 +102,7 @@ class OnboardingStepSwitcher extends PureComponent { onRemoveConfigValue={onRemoveConfigValue} onSaveTelegrafConfig={onSaveTelegrafConfig} onSetActiveTelegrafPlugin={onSetActiveTelegrafPlugin} + onSetConfigArrayValue={onSetConfigArrayValue} /> )} diff --git a/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap b/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap index 9f8d225c6d..637225cfb0 100644 --- a/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap +++ b/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap @@ -29,6 +29,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Admin username has been set" icon="checkmark" + id="" name="" onChange={[Function]} placeholder="" @@ -51,6 +52,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Admin password has been set" icon="checkmark" + id="" name="" onChange={[Function]} placeholder="" @@ -73,6 +75,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Admin password has been set" icon="checkmark" + id="" name="" onChange={[Function]} placeholder="" @@ -96,6 +99,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Default organization name has been set" icon="checkmark" + id="" name="" onChange={[Function]} placeholder="Your organization is where everything you create lives" @@ -118,6 +122,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Default bucket name has been set" icon="checkmark" + id="" name="" onChange={[Function]} placeholder="Your bucket is where you will store all your data" diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx index db7473dbfe..3ff62a8364 100644 --- a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.test.tsx @@ -37,6 +37,7 @@ const setup = (override = {}) => { location: null, router: null, routes: [], + onSetConfigArrayValue: jest.fn(), ...override, } diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx index 16a8874cee..65d10cc3d0 100644 --- a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx @@ -21,6 +21,7 @@ import { addConfigValue, removeConfigValue, createTelegrafConfigAsync, + setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Constants @@ -48,6 +49,7 @@ export interface OwnProps extends OnboardingStepProps { onRemoveConfigValue: typeof removeConfigValue onSaveTelegrafConfig: typeof createTelegrafConfigAsync authToken: string + onSetConfigArrayValue: typeof setConfigArrayValue } interface RouterProps { @@ -87,6 +89,7 @@ export class ConfigureDataSourceStep extends PureComponent { onSetPluginConfiguration, onAddConfigValue, onRemoveConfigValue, + onSetConfigArrayValue, } = this.props return ( @@ -103,6 +106,7 @@ export class ConfigureDataSourceStep extends PureComponent { dataLoaderType={type} currentIndex={+substepID} authToken={authToken} + onSetConfigArrayValue={onSetConfigArrayValue} />
diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx index 815f967c46..0a091ec013 100644 --- a/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx @@ -14,6 +14,7 @@ import { setPluginConfiguration, addConfigValue, removeConfigValue, + setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Types @@ -31,6 +32,7 @@ export interface Props { bucket: string org: string username: string + onSetConfigArrayValue: typeof setConfigArrayValue } @ErrorHandling @@ -47,6 +49,7 @@ class ConfigureDataSourceSwitcher extends PureComponent { onSetPluginConfiguration, onAddConfigValue, onRemoveConfigValue, + onSetConfigArrayValue, } = this.props switch (dataLoaderType) { @@ -60,6 +63,7 @@ class ConfigureDataSourceSwitcher extends PureComponent { currentIndex={currentIndex} onAddConfigValue={onAddConfigValue} authToken={authToken} + onSetConfigArrayValue={onSetConfigArrayValue} /> ) case DataLoaderType.LineProtocol: diff --git a/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.test.tsx b/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.test.tsx index a077bd9e78..f81bcdcb13 100644 --- a/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.test.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.test.tsx @@ -5,7 +5,9 @@ import {shallow} from 'enzyme' // Components import ArrayFormElement from 'src/onboarding/components/configureStep/streaming/ArrayFormElement' import {FormElement} from 'src/clockface' -import TagInput from 'src/shared/components/TagInput' +import MultipleInput from './MultipleInput' + +import {TelegrafPluginInputCpu} from 'src/api' const setup = (override = {}) => { const props = { @@ -15,6 +17,8 @@ const setup = (override = {}) => { autoFocus: true, value: [], helpText: '', + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, ...override, } @@ -28,10 +32,10 @@ describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => const fieldName = 'yo' const {wrapper} = setup({fieldName}) const formElement = wrapper.find(FormElement) - const tagInput = wrapper.find(TagInput) + const multipleInput = wrapper.find(MultipleInput) expect(wrapper.exists()).toBe(true) expect(formElement.exists()).toBe(true) - expect(tagInput.exists()).toBe(true) + expect(multipleInput.exists()).toBe(true) }) }) diff --git a/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.tsx b/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.tsx index 643bf7cb3c..d931bd4210 100644 --- a/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/ArrayFormElement.tsx @@ -4,7 +4,13 @@ import _ from 'lodash' // Components import {Form} from 'src/clockface' -import TagInput, {Item} from 'src/shared/components/TagInput' +import MultipleInput, {Item} from './MultipleInput' + +// Actions +import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders' + +// Types +import {TelegrafPluginName} from 'src/types/v2/dataLoaders' interface Props { fieldName: string @@ -13,31 +19,42 @@ interface Props { autoFocus: boolean value: string[] helpText: string + onSetConfigArrayValue: typeof setConfigArrayValue + telegrafPluginName: TelegrafPluginName } -class ConfigFieldSwitcher extends PureComponent { +class ArrayFormElement extends PureComponent { public render() { - const {fieldName, autoFocus, helpText} = this.props - + const { + fieldName, + autoFocus, + helpText, + onSetConfigArrayValue, + telegrafPluginName, + } = this.props return ( - - - +
+ + + +
) } - private handleAddTag = (item: string) => { + private handleAddRow = (item: string) => { this.props.addTagValue(item, this.props.fieldName) } - private handleRemoveTag = (item: string) => { + private handleRemoveRow = (item: string) => { const {removeTagValue, fieldName} = this.props removeTagValue(item, fieldName) @@ -45,11 +62,10 @@ class ConfigFieldSwitcher extends PureComponent { private get tags(): Item[] { const {value} = this.props - return value.map(v => { return {text: v, name: v} }) } } -export default ConfigFieldSwitcher +export default ArrayFormElement diff --git a/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.test.tsx b/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.test.tsx index 71340ca5cc..3cc064bd4f 100644 --- a/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.test.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.test.tsx @@ -10,6 +10,7 @@ import {Input, FormElement} from 'src/clockface' // Types import {ConfigFieldType} from 'src/types/v2/dataLoaders' +import {TelegrafPluginInputCpu} from 'src/api' const setup = (override = {}, shouldMount = false) => { const props = { @@ -21,6 +22,8 @@ const setup = (override = {}, shouldMount = false) => { index: 0, value: '', isRequired: true, + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, ...override, } diff --git a/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.tsx b/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.tsx index 3eb3b3c61f..9b7e8cfce1 100644 --- a/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/ConfigFieldSwitcher.tsx @@ -8,7 +8,10 @@ import URIFormElement from 'src/shared/components/URIFormElement' import ArrayFormElement from 'src/onboarding/components/configureStep/streaming/ArrayFormElement' // Types -import {ConfigFieldType} from 'src/types/v2/dataLoaders' +import {ConfigFieldType, TelegrafPluginName} from 'src/types/v2/dataLoaders' + +// Actions +import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders' interface Props { fieldName: string @@ -19,11 +22,20 @@ interface Props { removeTagValue: (item: string, fieldName: string) => void value: string | string[] isRequired: boolean + onSetConfigArrayValue: typeof setConfigArrayValue + telegrafPluginName: TelegrafPluginName } class ConfigFieldSwitcher extends PureComponent { public render() { - const {fieldType, fieldName, onChange, value} = this.props + const { + fieldType, + fieldName, + onChange, + value, + onSetConfigArrayValue, + telegrafPluginName, + } = this.props switch (fieldType) { case ConfigFieldType.Uri: @@ -47,6 +59,8 @@ class ConfigFieldSwitcher extends PureComponent { autoFocus={this.autoFocus} value={value as string[]} helpText={this.optionalText} + onSetConfigArrayValue={onSetConfigArrayValue} + telegrafPluginName={telegrafPluginName} /> ) case ConfigFieldType.String: diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleInput.scss b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.scss new file mode 100644 index 0000000000..2f217722a5 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.scss @@ -0,0 +1,11 @@ +.multiple-input-index{ + align-content: center; + padding: 10px; + background-color: $g4-onyx; + width: 600px; + height: 300px; + margin-left: 100px auto; +} +.input-row--remove{ + align-content: left +} \ No newline at end of file diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleInput.test.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.test.tsx new file mode 100644 index 0000000000..f2fde3acfc --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.test.tsx @@ -0,0 +1,38 @@ +// Libraries +import React from 'react' +import {shallow} from 'enzyme' + +// Components +import MultipleInput from './MultipleInput' +import MultipleRow from './MultipleRow' + +import {TelegrafPluginInputCpu} from 'src/api' + +const setup = (override = {}) => { + const props = { + title: '', + displayTitle: false, + onAddRow: jest.fn(), + onDeleteRow: jest.fn(), + autoFocus: true, + tags: [], + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, + ...override, + } + + const wrapper = shallow() + + return {wrapper} +} + +describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => { + it('renders', () => { + const fieldName = 'yo' + const {wrapper} = setup({fieldName}) + const multipleRow = wrapper.find(MultipleRow) + + expect(wrapper.exists()).toBe(true) + expect(multipleRow.exists()).toBe(true) + }) +}) diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx new file mode 100644 index 0000000000..36e92373d0 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx @@ -0,0 +1,116 @@ +// Libraries +import React, {PureComponent, ChangeEvent} from 'react' +import _ from 'lodash' + +// Components +import Rows from './MultipleRow' +import {ErrorHandling} from 'src/shared/decorators/errors' +import {Input, InputType, AutoComplete} from 'src/clockface' + +// Actions +import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders' + +// Types +import {TelegrafPluginName} from 'src/types/v2/dataLoaders' + +export interface Item { + text?: string + name?: string +} +interface Props { + onAddRow: (item: string) => void + onDeleteRow: (item: string) => void + tags: Item[] + title: string + displayTitle: boolean + inputID?: string + autoFocus?: boolean + onSetConfigArrayValue: typeof setConfigArrayValue + telegrafPluginName: TelegrafPluginName +} + +interface State { + editingText: string +} + +@ErrorHandling +class MultipleInput extends PureComponent { + constructor(props: Props) { + super(props) + this.state = {editingText: ''} + } + + public render() { + const { + title, + tags, + autoFocus, + onSetConfigArrayValue, + telegrafPluginName, + } = this.props + const {editingText} = this.state + + return ( +
+ {this.label} + + +
+ ) + } + + private handleInputChange = (e: ChangeEvent) => { + this.setState({editingText: e.target.value}) + } + + private get id(): string { + const {title, inputID} = this.props + return inputID || title + } + + private get label(): JSX.Element { + const {title, displayTitle} = this.props + + if (displayTitle) { + return + } + } + + private handleKeyDown = e => { + if (e.key === 'Enter') { + e.preventDefault() + const newItem = e.target.value.trim() + const {tags, onAddRow} = this.props + if (!this.shouldAddToList(newItem, tags)) { + return + } + this.setState({editingText: ''}) + onAddRow(e.target.value) + } + } + + private handleDeleteRow = (item: Item) => { + this.props.onDeleteRow(item.name || item.text) + } + + private shouldAddToList(item: Item, tags: Item[]): boolean { + return !_.isEmpty(item) && !tags.find(l => l === item) + } +} + +export default MultipleInput diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleRow.test.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleRow.test.tsx new file mode 100644 index 0000000000..6cb4528674 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleRow.test.tsx @@ -0,0 +1,33 @@ +// Libraries +import React from 'react' +import {shallow} from 'enzyme' + +// Components +import MultipleRow from './MultipleRow' + +import {TelegrafPluginInputCpu} from 'src/api' + +const setup = (override = {}) => { + const props = { + confirmText: '', + onDeleteTag: jest.fn(), + fieldName: '', + tags: [], + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, + ...override, + } + + const wrapper = shallow() + + return {wrapper} +} + +describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => { + it('renders', () => { + const fieldName = 'yo' + const {wrapper} = setup({fieldName}) + + expect(wrapper.exists()).toBe(true) + }) +}) diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleRow.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleRow.tsx new file mode 100644 index 0000000000..57eed584b6 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleRow.tsx @@ -0,0 +1,113 @@ +// Libraries +import React, {PureComponent, SFC} from 'react' +import uuid from 'uuid' +import {ErrorHandling} from 'src/shared/decorators/errors' + +// Components +import {IndexList, ComponentColor} from 'src/clockface' +import InputClickToEdit from 'src/shared/components/InputClickToEdit' +import Context from 'src/clockface/components/context_menu/Context' + +// Types +import {IconFont} from 'src/clockface/types' +import {TelegrafPluginName} from 'src/types/v2/dataLoaders' + +// Actions +import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders' + +interface Item { + text?: string + name?: string +} + +interface RowsProps { + tags: Item[] + confirmText?: string + onDeleteTag?: (item: Item) => void + onSetConfigArrayValue: typeof setConfigArrayValue + fieldName: string + telegrafPluginName: TelegrafPluginName +} + +const Rows: SFC = ({ + tags, + onDeleteTag, + onSetConfigArrayValue, + fieldName, + telegrafPluginName, +}) => { + return ( +
+ {tags.map(item => { + return ( + + ) + })} +
+ ) +} + +interface RowProps { + confirmText?: string + item: Item + onDelete: (item: Item) => void + onSetConfigArrayValue: typeof setConfigArrayValue + fieldName: string + telegrafPluginName: TelegrafPluginName + index: number +} + +@ErrorHandling +class Row extends PureComponent { + public static defaultProps: Partial = { + confirmText: 'Delete', + } + + public render() { + const {item} = this.props + return ( + + } columnCount={2}> + + + + + + + + + + + + + ) + } + private handleClickDelete = item => () => { + this.props.onDelete(item) + } + private handleKeyDown = (value: string) => { + const { + telegrafPluginName, + fieldName, + onSetConfigArrayValue, + index, + } = this.props + onSetConfigArrayValue(telegrafPluginName, fieldName, index, value) + } +} + +export default Rows diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleRows.test.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleRows.test.tsx new file mode 100644 index 0000000000..9654e56f9d --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleRows.test.tsx @@ -0,0 +1,33 @@ +// Libraries +import React from 'react' +import {shallow} from 'enzyme' + +// Components +import MultipleRows from './MultipleRows' + +import {TelegrafPluginInputCpu} from 'src/api' + +const setup = (override = {}) => { + const props = { + confirmText: '', + onDeleteTag: jest.fn(), + fieldName: '', + tags: [], + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, + ...override, + } + + const wrapper = shallow() + + return {wrapper} +} + +describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => { + it('renders', () => { + const fieldName = 'yo' + const {wrapper} = setup({fieldName}) + + expect(wrapper.exists()).toBe(true) + }) +}) diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleRows.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleRows.tsx new file mode 100644 index 0000000000..40daf59087 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleRows.tsx @@ -0,0 +1,54 @@ +// Libraries +import React, {SFC} from 'react' +import uuid from 'uuid' + +// Components +import Row from 'src/onboarding/components/configureStep/streaming/Row' + +// Types +import {TelegrafPluginName} from 'src/types/v2/dataLoaders' + +// Actions +import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders' + +interface Item { + text?: string + name?: string +} + +interface RowsProps { + tags: Item[] + confirmText?: string + onDeleteTag?: (item: Item) => void + onSetConfigArrayValue: typeof setConfigArrayValue + fieldName: string + telegrafPluginName: TelegrafPluginName +} + +const Rows: SFC = ({ + tags, + onDeleteTag, + onSetConfigArrayValue, + fieldName, + telegrafPluginName, +}) => { + return ( +
+ {tags.map(item => { + return ( + + ) + })} +
+ ) +} + +export default Rows diff --git a/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.test.tsx b/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.test.tsx index a8ef89b2e6..75669a2379 100644 --- a/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.test.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.test.tsx @@ -24,6 +24,8 @@ const setup = (override = {}) => { onAddConfigValue: jest.fn(), onRemoveConfigValue: jest.fn(), authToken: '', + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, ...override, } diff --git a/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx b/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx index 807779d5e0..7e65014307 100644 --- a/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx @@ -12,6 +12,7 @@ import { addConfigValue, removeConfigValue, setPluginConfiguration, + setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Types @@ -29,6 +30,7 @@ interface Props { onAddConfigValue: typeof addConfigValue onRemoveConfigValue: typeof removeConfigValue authToken: string + onSetConfigArrayValue: typeof setConfigArrayValue } class PluginConfigForm extends PureComponent { @@ -45,7 +47,7 @@ class PluginConfigForm extends PureComponent { } private get formFields(): JSX.Element[] | JSX.Element { - const {configFields, telegrafPlugin} = this.props + const {configFields, telegrafPlugin, onSetConfigArrayValue} = this.props if (!configFields) { return

No configuration required.

@@ -64,6 +66,8 @@ class PluginConfigForm extends PureComponent { isRequired={isRequired} addTagValue={this.handleAddConfigFieldValue} removeTagValue={this.handleRemoveConfigFieldValue} + onSetConfigArrayValue={onSetConfigArrayValue} + telegrafPluginName={telegrafPlugin.name} /> ) } @@ -99,7 +103,6 @@ class PluginConfigForm extends PureComponent { } else { defaultEmpty = [] } - return _.get(telegrafPlugin, `plugin.config.${fieldName}`, defaultEmpty) } diff --git a/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.test.tsx b/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.test.tsx index 53da438cbe..8c09eb52f4 100644 --- a/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.test.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.test.tsx @@ -9,6 +9,7 @@ import PluginConfigForm from 'src/onboarding/components/configureStep/streaming/ // Constants import {telegrafPlugin, token} from 'mocks/dummyData' +import {TelegrafPluginInputCpu} from 'src/api' const setup = (override = {}) => { const props = { @@ -19,6 +20,8 @@ const setup = (override = {}) => { onSetPluginConfiguration: jest.fn(), onAddConfigValue: jest.fn(), onRemoveConfigValue: jest.fn(), + onSetConfigArrayValue: jest.fn(), + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, ...override, } diff --git a/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx b/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx index 251b85db1c..6c7398ccf0 100644 --- a/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx @@ -15,6 +15,7 @@ import { setPluginConfiguration, addConfigValue, removeConfigValue, + setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Types @@ -28,6 +29,7 @@ interface Props { onRemoveConfigValue: typeof removeConfigValue currentIndex: number authToken: string + onSetConfigArrayValue: typeof setConfigArrayValue } class PluginConfigSwitcher extends PureComponent { @@ -38,6 +40,7 @@ class PluginConfigSwitcher extends PureComponent { onSetPluginConfiguration, onAddConfigValue, onRemoveConfigValue, + onSetConfigArrayValue, } = this.props if (this.currentTelegrafPlugin) { @@ -50,6 +53,7 @@ class PluginConfigSwitcher extends PureComponent { configFields={this.configFields} onAddConfigValue={onAddConfigValue} onRemoveConfigValue={onRemoveConfigValue} + onSetConfigArrayValue={onSetConfigArrayValue} /> ) } diff --git a/ui/src/onboarding/components/configureStep/streaming/Row.test.tsx b/ui/src/onboarding/components/configureStep/streaming/Row.test.tsx new file mode 100644 index 0000000000..f1802a4163 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/Row.test.tsx @@ -0,0 +1,34 @@ +// Libraries +import React from 'react' +import {shallow} from 'enzyme' + +// Components +import Row from './Row' + +import {TelegrafPluginInputCpu} from 'src/api' + +const setup = (override = {}) => { + const props = { + confirmText: '', + item: {}, + onDeleteTag: jest.fn(), + onSetConfigArrayValue: jest.fn(), + fieldName: '', + telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu, + index: 0, + ...override, + } + + const wrapper = shallow() + + return {wrapper} +} + +describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => { + it('renders', () => { + const fieldName = 'yo' + const {wrapper} = setup({fieldName}) + + expect(wrapper.exists()).toBe(true) + }) +}) diff --git a/ui/src/onboarding/components/configureStep/streaming/Row.tsx b/ui/src/onboarding/components/configureStep/streaming/Row.tsx new file mode 100644 index 0000000000..b9b538efe5 --- /dev/null +++ b/ui/src/onboarding/components/configureStep/streaming/Row.tsx @@ -0,0 +1,78 @@ +// Libraries +import React, {PureComponent} from 'react' +import uuid from 'uuid' +import {ErrorHandling} from 'src/shared/decorators/errors' + +// Components +import {IndexList, ComponentColor} from 'src/clockface' +import InputClickToEdit from 'src/shared/components/InputClickToEdit' +import Context from 'src/clockface/components/context_menu/Context' + +// Types +import {IconFont} from 'src/clockface/types' +import {TelegrafPluginName} from 'src/types/v2/dataLoaders' + +// Actions +import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders' + +interface Item { + text?: string + name?: string +} + +interface RowProps { + confirmText?: string + item: Item + onDelete: (item: Item) => void + onSetConfigArrayValue: typeof setConfigArrayValue + fieldName: string + telegrafPluginName: TelegrafPluginName + index: number +} + +@ErrorHandling +class Row extends PureComponent { + public static defaultProps: Partial = { + confirmText: 'Delete', + } + + public render() { + const {item} = this.props + return ( + + } columnCount={2}> + + + + + + + + + + + + + ) + } + private handleClickDelete = item => () => { + this.props.onDelete(item) + } + private handleKeyDown = (value: string) => { + const { + telegrafPluginName, + fieldName, + onSetConfigArrayValue, + index, + } = this.props + onSetConfigArrayValue(telegrafPluginName, fieldName, index, value) + } +} + +export default Row diff --git a/ui/src/onboarding/containers/OnboardingWizard.tsx b/ui/src/onboarding/containers/OnboardingWizard.tsx index eb00780cc0..a26a109072 100644 --- a/ui/src/onboarding/containers/OnboardingWizard.tsx +++ b/ui/src/onboarding/containers/OnboardingWizard.tsx @@ -27,6 +27,7 @@ import { createTelegrafConfigAsync, addPluginBundleWithPlugins, removePluginBundleWithPlugins, + setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Constants @@ -81,6 +82,7 @@ interface DispatchProps { onSetActiveTelegrafPlugin: typeof setActiveTelegrafPlugin onSetPluginConfiguration: typeof setPluginConfiguration onSaveTelegrafConfig: typeof createTelegrafConfigAsync + onSetConfigArrayValue: typeof setConfigArrayValue } interface StateProps { @@ -125,6 +127,7 @@ class OnboardingWizard extends PureComponent { onRemovePluginBundle, setupParams, notify, + onSetConfigArrayValue, } = this.props return ( @@ -156,6 +159,7 @@ class OnboardingWizard extends PureComponent { onSaveTelegrafConfig={onSaveTelegrafConfig} onAddPluginBundle={onAddPluginBundle} onRemovePluginBundle={onRemovePluginBundle} + onSetConfigArrayValue={onSetConfigArrayValue} />
@@ -297,6 +301,7 @@ const mdtp: DispatchProps = { onAddPluginBundle: addPluginBundleWithPlugins, onRemovePluginBundle: removePluginBundleWithPlugins, onSetPluginConfiguration: setPluginConfiguration, + onSetConfigArrayValue: setConfigArrayValue, } export default connect( diff --git a/ui/src/onboarding/reducers/dataLoaders.ts b/ui/src/onboarding/reducers/dataLoaders.ts index a6fb2e8326..976fd8f2c7 100644 --- a/ui/src/onboarding/reducers/dataLoaders.ts +++ b/ui/src/onboarding/reducers/dataLoaders.ts @@ -168,6 +168,28 @@ export default (state = INITIAL_STATE, action: Action): DataLoadersState => { return tp }), } + case 'SET_TELEGRAF_PLUGIN_CONFIG_VALUE': + return { + ...state, + telegrafPlugins: state.telegrafPlugins.map(tp => { + if (tp.name === action.payload.pluginName) { + const plugin = _.get(tp, 'plugin', createNewPlugin(tp.name)) + const configValues = _.get( + plugin, + `config.${action.payload.field}`, + [] + ) + configValues[action.payload.valueIndex] = action.payload.value + return { + ...tp, + plugin: updateConfigFields(plugin, action.payload.field, [ + ...configValues, + ]), + } + } + return tp + }), + } case 'SET_ACTIVE_TELEGRAF_PLUGIN': return { ...state, diff --git a/ui/src/shared/components/InputClickToEdit.scss b/ui/src/shared/components/InputClickToEdit.scss new file mode 100644 index 0000000000..0c672eba22 --- /dev/null +++ b/ui/src/shared/components/InputClickToEdit.scss @@ -0,0 +1,13 @@ + +@import 'src/style/modules'; + +.input-cte{ + padding:10px; + border: 1px solid; + border-radius: 3px; + border-color: $g2-kevlar; +} +.input-cte-span{ + padding:10px; + margin-right: 150px; +} \ No newline at end of file diff --git a/ui/src/shared/components/InputClickToEdit.tsx b/ui/src/shared/components/InputClickToEdit.tsx index 65bf94c6e6..2e7260bdd8 100644 --- a/ui/src/shared/components/InputClickToEdit.tsx +++ b/ui/src/shared/components/InputClickToEdit.tsx @@ -1,10 +1,12 @@ import React, {ChangeEvent, KeyboardEvent, PureComponent} from 'react' import {ErrorHandling} from 'src/shared/decorators/errors' +import './InputClickToEdit.scss' interface Props { wrapperClass: string value?: string onChange?: (value: string) => void + onKeyDown?: (value: string) => void onBlur: (value: string) => void disabled?: boolean tabIndex?: number @@ -67,16 +69,18 @@ class InputClickToEdit extends PureComponent { } public handleKeyDown(e: KeyboardEvent) { - const {onBlur, value} = this.props - if (e.key === 'Enter') { - if (value !== e.currentTarget.value) { - onBlur(e.currentTarget.value) - } + const {onKeyDown, value} = this.props + if (onKeyDown) { + if (e.key === 'Enter') { + if (value !== e.currentTarget.value) { + onKeyDown(e.currentTarget.value) + } - this.setState({ - initialValue: e.currentTarget.value, - isEditing: false, - }) + this.setState({ + initialValue: e.currentTarget.value, + isEditing: false, + }) + } } if (e.key === 'Escape') { this.handleCancel() @@ -138,7 +142,7 @@ class InputClickToEdit extends PureComponent { onFocus={this.handleInputClick} tabIndex={tabIndex} > - {value || placeholder} + {value || placeholder} {appearAsNormalInput || ( )} From 61e36cbee610fbcbb9f81bc14748824c3c30f197 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 18 Dec 2018 11:04:42 -0700 Subject: [PATCH 03/14] chore(Makefile): add target to check generated files are accurate A standard Makefile is used now in all subdirs that run go generate. Make will only generate the file if its source files changed. The checkgenerate target runs clean to ensure all targets a generated fresh. --- Makefile | 30 ++++++++---------------- chronograf/Makefile | 26 +++++++++++++++++++++ chronograf/canned/Makefile | 26 +++++++++++++++++++++ chronograf/dist/Makefile | 26 +++++++++++++++++++++ chronograf/server/Makefile | 26 +++++++++++++++++++++ etc/checkgenerate.sh | 13 +++++++++++ http/Makefile | 24 ++++++++++++++++++++ query/Makefile | 22 ++++++++++++++---- query/promql/Makefile | 27 +++++++++++++++++++--- storage/Makefile | 26 +++++++++++++++++++++ storage/reads/Makefile | 39 ++++++++++++++++++++++++++++++++ storage/reads/datatypes/Makefile | 30 ++++++++++++++++++++++++ task/Makefile | 22 +++++++++++++++--- task/backend/Makefile | 26 +++++++++++++++++---- 14 files changed, 327 insertions(+), 36 deletions(-) create mode 100644 chronograf/Makefile create mode 100644 chronograf/canned/Makefile create mode 100644 chronograf/dist/Makefile create mode 100644 chronograf/server/Makefile create mode 100755 etc/checkgenerate.sh create mode 100644 http/Makefile create mode 100644 storage/Makefile create mode 100644 storage/reads/Makefile create mode 100644 storage/reads/datatypes/Makefile diff --git a/Makefile b/Makefile index cc9e238ee0..efa7fe81ae 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,12 @@ # * All cmds must be added to this top level Makefile. # * All binaries are placed in ./bin, its recommended to add this directory to your PATH. # * Each package that has a need to run go generate, must have its own Makefile for that purpose. -# * All recursive Makefiles must support the all target +# * All recursive Makefiles must support the all and clean targets # -SUBDIRS := query task +# SUBDIRS are directories that have their own Makefile. +# It is required that all subdirs have the `all` and `clean` targets. +SUBDIRS := http ui query storage task GO_ARGS=-tags '$(GO_TAGS)' @@ -55,10 +57,6 @@ all: node_modules subdirs ui generate $(CMDS) subdirs: $(SUBDIRS) @for d in $^; do $(MAKE) -C $$d all; done - -ui: - $(MAKE) -C ui all - # # Define targets for commands # @@ -96,16 +94,10 @@ tidy: checktidy: ./etc/checktidy.sh -chronograf/dist/dist_gen.go: ui/build $(UISOURCES) - $(GO_GENERATE) ./chronograf/dist/... +checkgenerate: + ./etc/checkgenerate.sh -chronograf/server/swagger_gen.go: chronograf/server/swagger.json - $(GO_GENERATE) ./chronograf/server/... - -chronograf/canned/bin_gen.go: $(PRECANNED) - $(GO_GENERATE) ./chronograf/canned/... - -generate: chronograf/dist/dist_gen.go chronograf/server/swagger_gen.go chronograf/canned/bin_gen.go +generate: subdirs test-js: node_modules make -C ui test @@ -132,7 +124,7 @@ nightly: all env GO111MODULE=on go run github.com/goreleaser/goreleaser --snapshot --rm-dist --publish-snapshots clean: - $(MAKE) -C ui $(MAKECMDGOALS) + @for d in $(SUBDIRS); do $(MAKE) -C $$d clean; done rm -rf bin @@ -153,10 +145,6 @@ chronogiraffe: subdirs generate $(CMDS) run: chronogiraffe ./bin/$(GOOS)/influxd --developer-mode=true -generate-typescript-client: - cat http/cur_swagger.yml | go run ./internal/yaml2json > openapi.json - openapi-generator generate -g typescript-axios -o ui/src/api -i openapi.json - rm openapi.json # .PHONY targets represent actions that do not create an actual file. -.PHONY: all subdirs $(SUBDIRS) ui run fmt checkfmt tidy checktidy test test-go test-js test-go-race bench clean node_modules vet nightly chronogiraffe +.PHONY: all subdirs $(SUBDIRS) run fmt checkfmt tidy checktidy checkgenerate test test-go test-js test-go-race bench clean node_modules vet nightly chronogiraffe diff --git a/chronograf/Makefile b/chronograf/Makefile new file mode 100644 index 0000000000..ac1ccb08fe --- /dev/null +++ b/chronograf/Makefile @@ -0,0 +1,26 @@ +# List any generated files here +TARGETS = +# List any source files used to generate the targets here +SOURCES = +# List any directories that have their own Makefile here +SUBDIRS = dist server canned + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/chronograf/canned/Makefile b/chronograf/canned/Makefile new file mode 100644 index 0000000000..3d97f2e9f7 --- /dev/null +++ b/chronograf/canned/Makefile @@ -0,0 +1,26 @@ +# List any generated files here +TARGETS = bin_gen.go +# List any source files used to generate the targets here +SOURCES = bin.go +# List any directories that have their own Makefile here +SUBDIRS = + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/chronograf/dist/Makefile b/chronograf/dist/Makefile new file mode 100644 index 0000000000..c29ea054c6 --- /dev/null +++ b/chronograf/dist/Makefile @@ -0,0 +1,26 @@ +# List any generated files here +TARGETS = dist_gen.go +# List any source files used to generate the targets here +SOURCES = dist.go +# List any directories that have their own Makefile here +SUBDIRS = + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/chronograf/server/Makefile b/chronograf/server/Makefile new file mode 100644 index 0000000000..ab0d95b654 --- /dev/null +++ b/chronograf/server/Makefile @@ -0,0 +1,26 @@ +# List any generated files here +TARGETS = swagger_gen.go +# List any source files used to generate the targets here +SOURCES = swagger.json +# List any directories that have their own Makefile here +SUBDIRS = + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/etc/checkgenerate.sh b/etc/checkgenerate.sh new file mode 100755 index 0000000000..8836e7cd54 --- /dev/null +++ b/etc/checkgenerate.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +make clean +make generate + +status=$(git status --porcelain) +if [ -n "$status" ]; then + >&2 echo "generated code is not accurate, please run make generate" + >&2 echo -e "Files changed:\n$status" + exit 1 +fi diff --git a/http/Makefile b/http/Makefile new file mode 100644 index 0000000000..3f041db143 --- /dev/null +++ b/http/Makefile @@ -0,0 +1,24 @@ +# List any generated files here +TARGETS = ../ui/src/api/api.ts +# List any source files used to generate the targets here +SOURCES = cur_swagger.yml +# List any directories that have their own Makefile here +SUBDIRS = + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +../ui/src/api/api.ts: $(SOURCES) + cat cur_swagger.yml | go run ../internal/yaml2json > openapi.json + openapi-generator generate -g typescript-axios -o ../ui/src/api -i openapi.json + rm openapi.json + +.PHONY: all clean $(SUBDIRS) diff --git a/query/Makefile b/query/Makefile index 6c04c76050..e3993d6e26 100644 --- a/query/Makefile +++ b/query/Makefile @@ -1,12 +1,26 @@ - +# List any generated files here +TARGETS = +# List any source files used to generate the targets here +SOURCES = +# List any directories that have their own Makefile here SUBDIRS = promql -subdirs: $(SUBDIRS) +# Default target +all: $(SUBDIRS) $(TARGETS) +# Recurse into subdirs for same make goal $(SUBDIRS): $(MAKE) -C $@ $(MAKECMDGOALS) -all: $(SUBDIRS) +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) -.PHONY: all subdirs $(SUBDIRS) +# Define go generate if not already defined +GO_GENERATE := go generate +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/query/promql/Makefile b/query/promql/Makefile index 2babd5c01b..5803716b80 100644 --- a/query/promql/Makefile +++ b/query/promql/Makefile @@ -1,8 +1,29 @@ -all: promql.go +# List any generated files here +TARGETS = promql.go +# List any source files used to generate the targets here +SOURCES = gen.go \ + promql.peg + +# List any directories that have their own Makefile here +SUBDIRS = + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined GO_GENERATE := go generate -promql.go: promql.peg gen.go +# Run go generate for the targets +$(TARGETS): $(SOURCES) $(GO_GENERATE) -x -.PHONY: all +.PHONY: all clean $(SUBDIRS) diff --git a/storage/Makefile b/storage/Makefile new file mode 100644 index 0000000000..1246ee9e98 --- /dev/null +++ b/storage/Makefile @@ -0,0 +1,26 @@ +# List any generated files here +TARGETS = +# List any source files used to generate the targets here +SOURCES = +# List any directories that have their own Makefile here +SUBDIRS = reads + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/storage/reads/Makefile b/storage/reads/Makefile new file mode 100644 index 0000000000..31128ead10 --- /dev/null +++ b/storage/reads/Makefile @@ -0,0 +1,39 @@ +# List any generated files here +TARGETS = array_cursor.gen.go \ + response_writer.gen.go \ + stream_reader.gen.go \ + stream_reader_gen_test.go \ + table.gen.go + +# List any source files used to generate the targets here +SOURCES = gen.go \ + array_cursor.gen.go.tmpl \ + array_cursor.gen.go.tmpldata \ + response_writer.gen.go.tmpl \ + stream_reader.gen.go.tmpl \ + stream_reader_gen_test.go.tmpl \ + table.gen.go.tmpl \ + types.tmpldata + +# List any directories that have their own Makefile here +SUBDIRS = datatypes + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/storage/reads/datatypes/Makefile b/storage/reads/datatypes/Makefile new file mode 100644 index 0000000000..9ec158ee62 --- /dev/null +++ b/storage/reads/datatypes/Makefile @@ -0,0 +1,30 @@ +# List any generated files here +TARGETS = predicate.pb.go \ + storage_common.pb.go + +# List any source files used to generate the targets here +SOURCES = gen.go \ + predicate.proto \ + storage_common.proto + +# List any directories that have their own Makefile here +SUBDIRS = + +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined +GO_GENERATE := go generate + +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/task/Makefile b/task/Makefile index 989f987487..c28ad5e07f 100644 --- a/task/Makefile +++ b/task/Makefile @@ -1,10 +1,26 @@ +# List any generated files here +TARGETS = +# List any source files used to generate the targets here +SOURCES = +# List any directories that have their own Makefile here SUBDIRS = backend -subdirs: $(SUBDIRS) +# Default target +all: $(SUBDIRS) $(TARGETS) +# Recurse into subdirs for same make goal $(SUBDIRS): $(MAKE) -C $@ $(MAKECMDGOALS) -all: $(SUBDIRS) +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) -.PHONY: all subdirs $(SUBDIRS) +# Define go generate if not already defined +GO_GENERATE := go generate + +# Run go generate for the targets +$(TARGETS): $(SOURCES) + $(GO_GENERATE) -x + +.PHONY: all clean $(SUBDIRS) diff --git a/task/backend/Makefile b/task/backend/Makefile index 96bb9a2c49..3a565abb59 100644 --- a/task/backend/Makefile +++ b/task/backend/Makefile @@ -1,10 +1,26 @@ -targets := meta.pb.go +# List any generated files here +TARGETS = meta.pb.go +# List any source files used to generate the targets here +SOURCES = meta.proto +# List any directories that have their own Makefile here +SUBDIRS = +# Default target +all: $(SUBDIRS) $(TARGETS) + +# Recurse into subdirs for same make goal +$(SUBDIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +# Clean all targets recursively +clean: $(SUBDIRS) + rm -f $(TARGETS) + +# Define go generate if not already defined GO_GENERATE := go generate -all: $(targets) - -$(targets): meta.proto +# Run go generate for the targets +$(TARGETS): $(SOURCES) $(GO_GENERATE) -x -.PHONY: all +.PHONY: all clean $(SUBDIRS) From 758af5eaafa38f8a97675f1ca9b19332b9bf509a Mon Sep 17 00:00:00 2001 From: Alirie Gray Date: Mon, 17 Dec 2018 11:18:16 -0800 Subject: [PATCH 04/14] Create/update telegraf plugin at verify step of onboarding --- ui/src/onboarding/actions/dataLoaders.ts | 32 ++++++++-- ui/src/onboarding/apis/index.ts | 16 +++++ .../components/OnboardingStepSwitcher.tsx | 26 +++++--- .../configureStep/ConfigureDataSourceStep.tsx | 21 ------- .../ConfigureDataSourceSwitcher.tsx | 3 - .../streaming/PluginConfigForm.tsx | 1 - .../streaming/PluginConfigSwitcher.tsx | 3 - ...test.tsx => CreateOrUpdateConfig.test.tsx} | 8 ++- .../verifyStep/CreateOrUpdateConfig.tsx | 63 +++++++++++++++++++ .../components/verifyStep/DataStreaming.tsx | 40 ++++++++---- .../components/verifyStep/FetchAuthToken.tsx | 1 + .../components/verifyStep/FetchConfigID.tsx | 51 --------------- .../verifyStep/VerifyDataStep.test.tsx | 3 + .../components/verifyStep/VerifyDataStep.tsx | 14 ++++- .../verifyStep/VerifyDataSwitcher.test.tsx | 3 + .../verifyStep/VerifyDataSwitcher.tsx | 21 ++++++- .../containers/OnboardingWizard.tsx | 6 +- .../onboarding/reducers/dataLoaders.test.ts | 4 +- 18 files changed, 202 insertions(+), 114 deletions(-) rename ui/src/onboarding/components/verifyStep/{FetchConfigID.test.tsx => CreateOrUpdateConfig.test.tsx} (61%) create mode 100644 ui/src/onboarding/components/verifyStep/CreateOrUpdateConfig.tsx delete mode 100644 ui/src/onboarding/components/verifyStep/FetchConfigID.tsx diff --git a/ui/src/onboarding/actions/dataLoaders.ts b/ui/src/onboarding/actions/dataLoaders.ts index d41cf71709..6300392ca1 100644 --- a/ui/src/onboarding/actions/dataLoaders.ts +++ b/ui/src/onboarding/actions/dataLoaders.ts @@ -5,6 +5,8 @@ import _ from 'lodash' import { writeLineProtocol, createTelegrafConfig, + getTelegrafConfigs, + updateTelegrafConfig, } from 'src/onboarding/apis/index' // Utils @@ -226,7 +228,7 @@ export const removePluginBundleWithPlugins = ( dispatch(removeBundlePlugins(bundle)) } -export const createTelegrafConfigAsync = (authToken: string) => async ( +export const createOrUpdateTelegrafConfigAsync = (authToken: string) => async ( dispatch, getState: GetState ) => { @@ -239,7 +241,28 @@ export const createTelegrafConfigAsync = (authToken: string) => async ( }, } = getState() - let plugins = telegrafPlugins.map(tp => tp.plugin || createNewPlugin(tp.name)) + const telegrafConfigsFromServer = await getTelegrafConfigs(org) + + let plugins = [] + telegrafPlugins.forEach(tp => { + if (tp.configured === ConfigurationState.Configured) { + plugins = [...plugins, tp.plugin || createNewPlugin(tp.name)] + } + }) + + let body = { + name: 'new config', + agent: {collectionInterval: DEFAULT_COLLECTION_INTERVAL}, + plugins, + } + + if (telegrafConfigsFromServer.length) { + const id = _.get(telegrafConfigsFromServer, '0.id', '') + + await updateTelegrafConfig(id, body) + dispatch(setTelegrafConfigID(id)) + return + } const influxDB2Out = { name: TelegrafPluginOutputInfluxDBV2.NameEnum.InfluxdbV2, @@ -254,9 +277,8 @@ export const createTelegrafConfigAsync = (authToken: string) => async ( plugins = [...plugins, influxDB2Out] - const body = { - name: 'new config', - agent: {collectionInterval: DEFAULT_COLLECTION_INTERVAL}, + body = { + ...body, plugins, } diff --git a/ui/src/onboarding/apis/index.ts b/ui/src/onboarding/apis/index.ts index b595608e30..83beddf821 100644 --- a/ui/src/onboarding/apis/index.ts +++ b/ui/src/onboarding/apis/index.ts @@ -130,3 +130,19 @@ export const createTelegrafConfig = async ( console.error(error) } } + +export const updateTelegrafConfig = async ( + telegrafID: string, + telegrafConfig: TelegrafRequest +) => { + try { + const {data} = await telegrafsAPI.telegrafsTelegrafIDPut( + telegrafID, + telegrafConfig + ) + + return data + } catch (error) { + console.error(error) + } +} diff --git a/ui/src/onboarding/components/OnboardingStepSwitcher.tsx b/ui/src/onboarding/components/OnboardingStepSwitcher.tsx index 9b0ce768a4..aeb90ecf1e 100644 --- a/ui/src/onboarding/components/OnboardingStepSwitcher.tsx +++ b/ui/src/onboarding/components/OnboardingStepSwitcher.tsx @@ -19,7 +19,7 @@ import { setActiveTelegrafPlugin, addConfigValue, removeConfigValue, - createTelegrafConfigAsync, + createOrUpdateTelegrafConfigAsync, addPluginBundleWithPlugins, removePluginBundleWithPlugins, setPluginConfiguration, @@ -42,7 +42,7 @@ interface Props { setupParams: SetupParams dataLoaders: DataLoadersState currentStepIndex: number - onSaveTelegrafConfig: typeof createTelegrafConfigAsync + onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync onAddPluginBundle: typeof addPluginBundleWithPlugins onRemovePluginBundle: typeof removePluginBundleWithPlugins onSetConfigArrayValue: typeof setConfigArrayValue @@ -100,7 +100,6 @@ class OnboardingStepSwitcher extends PureComponent { onSetPluginConfiguration={onSetPluginConfiguration} onAddConfigValue={onAddConfigValue} onRemoveConfigValue={onRemoveConfigValue} - onSaveTelegrafConfig={onSaveTelegrafConfig} onSetActiveTelegrafPlugin={onSetActiveTelegrafPlugin} onSetConfigArrayValue={onSetConfigArrayValue} /> @@ -109,12 +108,21 @@ class OnboardingStepSwitcher extends PureComponent { ) case 4: return ( - + + {authToken => ( + + )} + ) case 5: return diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx index 65d10cc3d0..ddee0a6683 100644 --- a/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceStep.tsx @@ -20,16 +20,11 @@ import { setPluginConfiguration, addConfigValue, removeConfigValue, - createTelegrafConfigAsync, setConfigArrayValue, } from 'src/onboarding/actions/dataLoaders' // Constants import {StepStatus} from 'src/clockface/constants/wizard' -import { - TelegrafConfigCreationSuccess, - TelegrafConfigCreationError, -} from 'src/shared/copy/notifications' // Types import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard' @@ -47,7 +42,6 @@ export interface OwnProps extends OnboardingStepProps { type: DataLoaderType onAddConfigValue: typeof addConfigValue onRemoveConfigValue: typeof removeConfigValue - onSaveTelegrafConfig: typeof createTelegrafConfigAsync authToken: string onSetConfigArrayValue: typeof setConfigArrayValue } @@ -82,7 +76,6 @@ export class ConfigureDataSourceStep extends PureComponent { const { telegrafPlugins, type, - authToken, params: {substepID}, setupParams, onUpdateTelegrafPluginConfig, @@ -105,7 +98,6 @@ export class ConfigureDataSourceStep extends PureComponent { onRemoveConfigValue={onRemoveConfigValue} dataLoaderType={type} currentIndex={+substepID} - authToken={authToken} onSetConfigArrayValue={onSetConfigArrayValue} />
@@ -201,11 +193,7 @@ export class ConfigureDataSourceStep extends PureComponent { onSetActiveTelegrafPlugin, onSetPluginConfiguration, telegrafPlugins, - authToken, - notify, params: {substepID, stepID}, - type, - onSaveTelegrafConfig, onSetSubstepIndex, } = this.props @@ -216,15 +204,6 @@ export class ConfigureDataSourceStep extends PureComponent { this.handleSetStepStatus() if (index >= telegrafPlugins.length - 1) { - if (type === DataLoaderType.Streaming) { - try { - await onSaveTelegrafConfig(authToken) - notify(TelegrafConfigCreationSuccess) - } catch (error) { - notify(TelegrafConfigCreationError) - } - } - onIncrementCurrentStepIndex() onSetActiveTelegrafPlugin('') } else { diff --git a/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx b/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx index 0a091ec013..686a811c59 100644 --- a/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx +++ b/ui/src/onboarding/components/configureStep/ConfigureDataSourceSwitcher.tsx @@ -28,7 +28,6 @@ export interface Props { onAddConfigValue: typeof addConfigValue onRemoveConfigValue: typeof removeConfigValue dataLoaderType: DataLoaderType - authToken: string bucket: string org: string username: string @@ -41,7 +40,6 @@ class ConfigureDataSourceSwitcher extends PureComponent { const { bucket, org, - authToken, telegrafPlugins, currentIndex, dataLoaderType, @@ -62,7 +60,6 @@ class ConfigureDataSourceSwitcher extends PureComponent { telegrafPlugins={telegrafPlugins} currentIndex={currentIndex} onAddConfigValue={onAddConfigValue} - authToken={authToken} onSetConfigArrayValue={onSetConfigArrayValue} /> ) diff --git a/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx b/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx index 7e65014307..6a084f27b5 100644 --- a/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/PluginConfigForm.tsx @@ -29,7 +29,6 @@ interface Props { onSetPluginConfiguration: typeof setPluginConfiguration onAddConfigValue: typeof addConfigValue onRemoveConfigValue: typeof removeConfigValue - authToken: string onSetConfigArrayValue: typeof setConfigArrayValue } diff --git a/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx b/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx index 6c7398ccf0..ea373a27dc 100644 --- a/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/PluginConfigSwitcher.tsx @@ -28,14 +28,12 @@ interface Props { onAddConfigValue: typeof addConfigValue onRemoveConfigValue: typeof removeConfigValue currentIndex: number - authToken: string onSetConfigArrayValue: typeof setConfigArrayValue } class PluginConfigSwitcher extends PureComponent { public render() { const { - authToken, onUpdateTelegrafPluginConfig, onSetPluginConfiguration, onAddConfigValue, @@ -46,7 +44,6 @@ class PluginConfigSwitcher extends PureComponent { if (this.currentTelegrafPlugin) { return ( require('src/onboarding/apis/mocks')) @@ -11,15 +11,17 @@ const setup = async (override = {}) => { const props = { org: 'default', children: jest.fn(), + onSaveTelegrafConfig: jest.fn(), + authToken: '', ...override, } - const wrapper = await shallow() + const wrapper = await shallow() return {wrapper} } -describe('FetchConfigID', () => { +describe('CreateOrUpdateConfig', () => { it('renders', async () => { const {wrapper} = await setup() expect(wrapper.exists()).toBe(true) diff --git a/ui/src/onboarding/components/verifyStep/CreateOrUpdateConfig.tsx b/ui/src/onboarding/components/verifyStep/CreateOrUpdateConfig.tsx new file mode 100644 index 0000000000..4c1163f9e9 --- /dev/null +++ b/ui/src/onboarding/components/verifyStep/CreateOrUpdateConfig.tsx @@ -0,0 +1,63 @@ +// Libraries +import React, {PureComponent} from 'react' +import _ from 'lodash' + +// Components +import {Spinner} from 'src/clockface' +import {ErrorHandling} from 'src/shared/decorators/errors' + +// Actions +import {notify} from 'src/shared/actions/notifications' +import {createOrUpdateTelegrafConfigAsync} from 'src/onboarding/actions/dataLoaders' + +// Constants +import { + TelegrafConfigCreationSuccess, + TelegrafConfigCreationError, +} from 'src/shared/copy/notifications' + +// Types +import {RemoteDataState} from 'src/types' + +export interface Props { + org: string + authToken: string + children: () => JSX.Element + onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync +} + +interface State { + loading: RemoteDataState +} + +@ErrorHandling +class CreateOrUpdateConfig extends PureComponent { + constructor(props: Props) { + super(props) + + this.state = {loading: RemoteDataState.NotStarted} + } + + public async componentDidMount() { + const {onSaveTelegrafConfig, authToken} = this.props + + this.setState({loading: RemoteDataState.Loading}) + + try { + await onSaveTelegrafConfig(authToken) + notify(TelegrafConfigCreationSuccess) + + this.setState({loading: RemoteDataState.Done}) + } catch (error) { + notify(TelegrafConfigCreationError) + } + } + + public render() { + return ( + {this.props.children()} + ) + } +} + +export default CreateOrUpdateConfig diff --git a/ui/src/onboarding/components/verifyStep/DataStreaming.tsx b/ui/src/onboarding/components/verifyStep/DataStreaming.tsx index 2d19eb5c65..e445260020 100644 --- a/ui/src/onboarding/components/verifyStep/DataStreaming.tsx +++ b/ui/src/onboarding/components/verifyStep/DataStreaming.tsx @@ -4,10 +4,13 @@ import _ from 'lodash' // Components import TelegrafInstructions from 'src/onboarding/components/verifyStep/TelegrafInstructions' -import FetchConfigID from 'src/onboarding/components/verifyStep/FetchConfigID' +import CreateOrUpdateConfig from 'src/onboarding/components/verifyStep/CreateOrUpdateConfig' import FetchAuthToken from 'src/onboarding/components/verifyStep/FetchAuthToken' import DataListening from 'src/onboarding/components/verifyStep/DataListening' +// Actions +import {createOrUpdateTelegrafConfigAsync} from 'src/onboarding/actions/dataLoaders' + // Constants import {StepStatus} from 'src/clockface/constants/wizard' @@ -17,22 +20,37 @@ import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { bucket: string org: string + configID: string username: string stepIndex: number + authToken: string onSetStepStatus: (index: number, status: StepStatus) => void + onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync } @ErrorHandling class DataStreaming extends PureComponent { public render() { + const { + authToken, + org, + username, + configID, + onSaveTelegrafConfig, + onSetStepStatus, + bucket, + stepIndex, + } = this.props + return ( <> - - {configID => ( - + + {() => ( + {authToken => ( { )} )} - + ) diff --git a/ui/src/onboarding/components/verifyStep/FetchAuthToken.tsx b/ui/src/onboarding/components/verifyStep/FetchAuthToken.tsx index bf97f84c36..def1e37288 100644 --- a/ui/src/onboarding/components/verifyStep/FetchAuthToken.tsx +++ b/ui/src/onboarding/components/verifyStep/FetchAuthToken.tsx @@ -36,6 +36,7 @@ class FetchAuthToken extends PureComponent { this.setState({loading: RemoteDataState.Loading}) const authToken = await getAuthorizationToken(username) + this.setState({authToken, loading: RemoteDataState.Done}) } diff --git a/ui/src/onboarding/components/verifyStep/FetchConfigID.tsx b/ui/src/onboarding/components/verifyStep/FetchConfigID.tsx deleted file mode 100644 index bb9eec2d05..0000000000 --- a/ui/src/onboarding/components/verifyStep/FetchConfigID.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// 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 { - 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 ( - - {this.props.children(this.state.configID)} - - ) - } -} - -export default FetchConfigID diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx index 4c2396ff97..8f9aab8419 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataStep.test.tsx @@ -19,6 +19,9 @@ const setup = (override = {}) => { type: DataLoaderType.Empty, telegrafPlugins: [], stepIndex: 4, + authToken: '', + telegrafConfigID: '', + onSaveTelegrafConfig: jest.fn(), onSetActiveTelegrafPlugin: jest.fn(), ...override, } diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx index ef41303b30..3ea0f2a349 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx @@ -13,7 +13,10 @@ import { import VerifyDataSwitcher from 'src/onboarding/components/verifyStep/VerifyDataSwitcher' // Actions -import {setActiveTelegrafPlugin} from 'src/onboarding/actions/dataLoaders' +import { + setActiveTelegrafPlugin, + createOrUpdateTelegrafConfigAsync, +} from 'src/onboarding/actions/dataLoaders' // Types import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard' @@ -21,8 +24,11 @@ import {DataLoaderType, TelegrafPlugin} from 'src/types/v2/dataLoaders' export interface Props extends OnboardingStepProps { type: DataLoaderType + authToken: string + telegrafConfigID: string telegrafPlugins: TelegrafPlugin[] onSetActiveTelegrafPlugin: typeof setActiveTelegrafPlugin + onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync stepIndex: number } @@ -31,7 +37,10 @@ class VerifyDataStep extends PureComponent { public render() { const { setupParams, + telegrafConfigID, + authToken, type, + onSaveTelegrafConfig, onIncrementCurrentStepIndex, onSetStepStatus, stepIndex, @@ -41,6 +50,9 @@ class VerifyDataStep extends PureComponent {
{ org: '', username: '', bucket: '', + authToken: '', + telegrafConfigID: '', + onSaveTelegrafConfig: jest.fn(), stepIndex: 4, onSetStepStatus: jest.fn(), ...override, diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx index 9c1833702a..a29748fbe2 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx @@ -5,6 +5,9 @@ import React, {PureComponent} from 'react' import {ErrorHandling} from 'src/shared/decorators/errors' import DataStreaming from 'src/onboarding/components/verifyStep/DataStreaming' +// Actions +import {createOrUpdateTelegrafConfigAsync} from 'src/onboarding/actions/dataLoaders' + // Constants import {StepStatus} from 'src/clockface/constants/wizard' @@ -17,22 +20,38 @@ export interface Props { username: string bucket: string stepIndex: number + authToken: string + telegrafConfigID: string + onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync onSetStepStatus: (index: number, status: StepStatus) => void } @ErrorHandling class VerifyDataSwitcher extends PureComponent { public render() { - const {org, username, bucket, type, stepIndex, onSetStepStatus} = this.props + const { + org, + username, + bucket, + type, + stepIndex, + onSetStepStatus, + authToken, + telegrafConfigID, + onSaveTelegrafConfig, + } = this.props switch (type) { case DataLoaderType.Streaming: return ( ) diff --git a/ui/src/onboarding/containers/OnboardingWizard.tsx b/ui/src/onboarding/containers/OnboardingWizard.tsx index a26a109072..60085e3286 100644 --- a/ui/src/onboarding/containers/OnboardingWizard.tsx +++ b/ui/src/onboarding/containers/OnboardingWizard.tsx @@ -24,7 +24,7 @@ import { removeConfigValue, setActiveTelegrafPlugin, setPluginConfiguration, - createTelegrafConfigAsync, + createOrUpdateTelegrafConfigAsync, addPluginBundleWithPlugins, removePluginBundleWithPlugins, setConfigArrayValue, @@ -81,8 +81,8 @@ interface DispatchProps { onRemoveConfigValue: typeof removeConfigValue onSetActiveTelegrafPlugin: typeof setActiveTelegrafPlugin onSetPluginConfiguration: typeof setPluginConfiguration - onSaveTelegrafConfig: typeof createTelegrafConfigAsync onSetConfigArrayValue: typeof setConfigArrayValue + onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync } interface StateProps { @@ -297,7 +297,7 @@ const mdtp: DispatchProps = { onAddConfigValue: addConfigValue, onRemoveConfigValue: removeConfigValue, onSetActiveTelegrafPlugin: setActiveTelegrafPlugin, - onSaveTelegrafConfig: createTelegrafConfigAsync, + onSaveTelegrafConfig: createOrUpdateTelegrafConfigAsync, onAddPluginBundle: addPluginBundleWithPlugins, onRemovePluginBundle: removePluginBundleWithPlugins, onSetPluginConfiguration: setPluginConfiguration, diff --git a/ui/src/onboarding/reducers/dataLoaders.test.ts b/ui/src/onboarding/reducers/dataLoaders.test.ts index 24bf5381d9..e443f0febc 100644 --- a/ui/src/onboarding/reducers/dataLoaders.test.ts +++ b/ui/src/onboarding/reducers/dataLoaders.test.ts @@ -18,7 +18,7 @@ import { removeBundlePlugins, addPluginBundleWithPlugins, removePluginBundleWithPlugins, - createTelegrafConfigAsync, + createOrUpdateTelegrafConfigAsync, setPluginConfiguration, } from 'src/onboarding/actions/dataLoaders' @@ -415,7 +415,7 @@ describe('dataLoader reducer', () => { }, }, }) - await createTelegrafConfigAsync(token)(dispatch, getState) + await createOrUpdateTelegrafConfigAsync(token)(dispatch, getState) expect(dispatch).toBeCalledWith(setTelegrafConfigID(telegrafConfig.id)) }) From 242d9e86e1d7d1eab526544eff4716fb9fbd28fc Mon Sep 17 00:00:00 2001 From: Alirie Gray Date: Tue, 18 Dec 2018 12:34:21 -0800 Subject: [PATCH 05/14] Create mock for telegraf update function --- ui/src/onboarding/apis/index.ts | 2 +- ui/src/onboarding/apis/mocks.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/src/onboarding/apis/index.ts b/ui/src/onboarding/apis/index.ts index 83beddf821..2133a7980a 100644 --- a/ui/src/onboarding/apis/index.ts +++ b/ui/src/onboarding/apis/index.ts @@ -134,7 +134,7 @@ export const createTelegrafConfig = async ( export const updateTelegrafConfig = async ( telegrafID: string, telegrafConfig: TelegrafRequest -) => { +): Promise => { try { const {data} = await telegrafsAPI.telegrafsTelegrafIDPut( telegrafID, diff --git a/ui/src/onboarding/apis/mocks.ts b/ui/src/onboarding/apis/mocks.ts index 5ce4f481d2..ff7b7c1f65 100644 --- a/ui/src/onboarding/apis/mocks.ts +++ b/ui/src/onboarding/apis/mocks.ts @@ -8,6 +8,9 @@ const telegrafsGet = jest.fn(() => Promise.resolve(getTelegrafConfigsResponse)) const telegrafsPost = jest.fn(() => Promise.resolve(createTelegrafConfigResponse) ) +const telegrafsTelegrafIDPut = jest.fn(() => + Promise.resolve(createTelegrafConfigResponse) +) const authorizationsGet = jest.fn(() => Promise.resolve(authResponse)) const setupPost = jest.fn(() => Promise.resolve()) const setupGet = jest.fn(() => Promise.resolve({data: {allowed: true}})) @@ -15,6 +18,7 @@ const setupGet = jest.fn(() => Promise.resolve({data: {allowed: true}})) export const telegrafsAPI = { telegrafsGet, telegrafsPost, + telegrafsTelegrafIDPut, } export const setupAPI = { From 28cea4d957010e3a3c9dddc3041c54d61fbb6db3 Mon Sep 17 00:00:00 2001 From: Michael Desa Date: Tue, 18 Dec 2018 10:44:25 -0500 Subject: [PATCH 06/14] feat(platform): add generic kv store Co-authored-by: Leonardo Di Donato Co-authored-by: Michael Desa feat(kv): add kv store interface for services feat(bolt): add boltdb implementation of kv.Store spike(platform): add kv backed user service feat(kv): add static cursor Note here that this operation cannot be transactionally done. This poses a bit of issues that will need to be worked out. fix(bolt): use error explicit error message squash: play with interface a bit fix(kv): remove commit and rollback from kv interface feat(inmem): add inmem kv store chore: add note for inmem transactions fix(bolt): remove call to tx in kv store tests feat(kv): add tests for static cursor doc(kv): add comments to store and associated interfaces doc(bolt): add comments to key value store feat(testing): add kv store tests test(testing): add conformance test for kv.Store test(inmem): add kv.Store conformance tests doc(inmem): add comments to key value store feat(inmem): remove CreateBucketIfNotExists from Tx interface feat(bolt): remove CreateBucketIfNotExists from Tx feat(inmem): remove CreateBucketIfNotExists from Tx doc(kv): add note to bucket interface about conditions methods can be called feat(kv): add context methods to kv.Tx feat(bolt): add context methods to bolt.Tx feat(inmem): add context methods to inmem.Tx test(kv): add contract tests for view/update transactions feat(kv): ensure that static cursor is always valid Co-authored-by: Leonardo Di Donato Co-authored-by: Michael Desa fix(kv): remove error from cursor methods test(kv): remove want errors from cursor test test(testing): add concurrent update test for kv.Store feat(kv): make kv user service an example service fix(testing): add concurrnent update test to the kv.Store contract tests test(platform): fix example kv service tests dep(platform): make platform tidy --- bolt/bbolt_test.go | 21 + bolt/kv.go | 219 +++++++++++ bolt/kv_test.go | 92 +++++ bolt/user_test.go | 5 +- go.mod | 7 +- go.sum | 14 +- inmem/kv.go | 203 ++++++++++ inmem/kv_test.go | 64 ++++ kv/cursor.go | 80 ++++ kv/cursor_test.go | 244 ++++++++++++ kv/example.go | 436 +++++++++++++++++++++ kv/store.go | 52 +++ testing/kv.go | 928 +++++++++++++++++++++++++++++++++++++++++++++ user.go | 4 +- 14 files changed, 2357 insertions(+), 12 deletions(-) create mode 100644 bolt/kv.go create mode 100644 bolt/kv_test.go create mode 100644 inmem/kv.go create mode 100644 inmem/kv_test.go create mode 100644 kv/cursor.go create mode 100644 kv/cursor_test.go create mode 100644 kv/example.go create mode 100644 kv/store.go create mode 100644 testing/kv.go diff --git a/bolt/bbolt_test.go b/bolt/bbolt_test.go index 43e00f800c..38af04ea0e 100644 --- a/bolt/bbolt_test.go +++ b/bolt/bbolt_test.go @@ -64,3 +64,24 @@ func TestClientOpen(t *testing.T) { t.Fatalf("unable to close database %s: %v", boltFile, err) } } + +func NewTestKVStore() (*bolt.KVStore, func(), error) { + f, err := ioutil.TempFile("", "influxdata-platform-bolt-") + if err != nil { + return nil, nil, errors.New("unable to open temporary boltdb file") + } + f.Close() + + path := f.Name() + s := bolt.NewKVStore(path) + if err := s.Open(context.TODO()); err != nil { + return nil, nil, err + } + + close := func() { + s.Close() + os.Remove(path) + } + + return s, close, nil +} diff --git a/bolt/kv.go b/bolt/kv.go new file mode 100644 index 0000000000..01e2a78aaa --- /dev/null +++ b/bolt/kv.go @@ -0,0 +1,219 @@ +package bolt + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + bolt "github.com/coreos/bbolt" + "github.com/influxdata/platform/kv" + "go.uber.org/zap" +) + +// KVStore is a kv.Store backed by boltdb. +type KVStore struct { + path string + db *bolt.DB + logger *zap.Logger +} + +// NewKVStore returns an instance of KVStore with the file at +// the provided path. +func NewKVStore(path string) *KVStore { + return &KVStore{ + path: path, + logger: zap.NewNop(), + } +} + +// Open creates boltDB file it doesn't exists and opens it otherwise. +func (s *KVStore) Open(ctx context.Context) error { + // Ensure the required directory structure exists. + if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil { + return fmt.Errorf("unable to create directory %s: %v", s.path, err) + } + + if _, err := os.Stat(s.path); err != nil && !os.IsNotExist(err) { + return err + } + + // Open database file. + db, err := bolt.Open(s.path, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return fmt.Errorf("unable to open boltdb file %v", err) + } + s.db = db + + s.logger.Info("Resources opened", zap.String("path", s.path)) + return nil +} + +// Close the connection to the bolt database +func (s *KVStore) Close() error { + if s.db != nil { + return s.db.Close() + } + return nil +} + +// WithLogger sets the logger on the store. +func (s *KVStore) WithLogger(l *zap.Logger) { + s.logger = l +} + +// WithDB sets the boltdb on the store. +func (s *KVStore) WithDB(db *bolt.DB) { + s.db = db +} + +// View opens up a view transaction against the store. +func (s *KVStore) View(fn func(tx kv.Tx) error) error { + return s.db.View(func(tx *bolt.Tx) error { + return fn(&Tx{ + tx: tx, + ctx: context.Background(), + }) + }) +} + +// Update opens up an update transaction against the store. +func (s *KVStore) Update(fn func(tx kv.Tx) error) error { + return s.db.Update(func(tx *bolt.Tx) error { + return fn(&Tx{ + tx: tx, + ctx: context.Background(), + }) + }) +} + +// Tx is a light wrapper around a boltdb transaction. It implements kv.Tx. +type Tx struct { + tx *bolt.Tx + ctx context.Context +} + +// Context returns the context for the transaction. +func (tx *Tx) Context() context.Context { + return tx.ctx +} + +// WithContext sets the context for the transaction. +func (tx *Tx) WithContext(ctx context.Context) { + tx.ctx = ctx +} + +// createBucketIfNotExists creates a bucket with the provided byte slice. +func (tx *Tx) createBucketIfNotExists(b []byte) (*Bucket, error) { + bkt, err := tx.tx.CreateBucketIfNotExists(b) + if err != nil { + return nil, err + } + return &Bucket{ + bucket: bkt, + }, nil +} + +// Bucket retrieves the bucket named b. +func (tx *Tx) Bucket(b []byte) (kv.Bucket, error) { + bkt := tx.tx.Bucket(b) + if bkt == nil { + return tx.createBucketIfNotExists(b) + } + return &Bucket{ + bucket: bkt, + }, nil +} + +// Bucket implements kv.Bucket. +type Bucket struct { + bucket *bolt.Bucket +} + +// Get retrieves the value at the provided key. +func (b *Bucket) Get(key []byte) ([]byte, error) { + val := b.bucket.Get(key) + if len(val) == 0 { + return nil, kv.ErrKeyNotFound + } + + return val, nil +} + +// Put sets the value at the provided key. +func (b *Bucket) Put(key []byte, value []byte) error { + err := b.bucket.Put(key, value) + if err == bolt.ErrTxNotWritable { + return kv.ErrTxNotWritable + } + return err +} + +// Delete removes the provided key. +func (b *Bucket) Delete(key []byte) error { + err := b.bucket.Delete(key) + if err == bolt.ErrTxNotWritable { + return kv.ErrTxNotWritable + } + return err +} + +// Cursor retrieves a cursor for iterating through the entries +// in the key value store. +func (b *Bucket) Cursor() (kv.Cursor, error) { + return &Cursor{ + cursor: b.bucket.Cursor(), + }, nil +} + +// Cursor is a struct for iterating through the entries +// in the key value store. +type Cursor struct { + cursor *bolt.Cursor +} + +// Seek seeks for the first key that matches the prefix provided. +func (c *Cursor) Seek(prefix []byte) ([]byte, []byte) { + k, v := c.cursor.Seek(prefix) + if len(v) == 0 { + return nil, nil + } + return k, v +} + +// First retrieves the first key value pair in the bucket. +func (c *Cursor) First() ([]byte, []byte) { + k, v := c.cursor.First() + if len(v) == 0 { + return nil, nil + } + return k, v +} + +// Last retrieves the last key value pair in the bucket. +func (c *Cursor) Last() ([]byte, []byte) { + k, v := c.cursor.Last() + if len(v) == 0 { + return nil, nil + } + return k, v +} + +// Next retrieves the next key in the bucket. +func (c *Cursor) Next() ([]byte, []byte) { + k, v := c.cursor.Next() + if len(v) == 0 { + return nil, nil + } + return k, v +} + +// Prev retrieves the previous key in the bucket. +func (c *Cursor) Prev() ([]byte, []byte) { + k, v := c.cursor.Prev() + if len(v) == 0 { + return nil, nil + } + return k, v +} diff --git a/bolt/kv_test.go b/bolt/kv_test.go new file mode 100644 index 0000000000..8b083518b7 --- /dev/null +++ b/bolt/kv_test.go @@ -0,0 +1,92 @@ +package bolt_test + +import ( + "context" + "testing" + + "github.com/influxdata/platform" + "github.com/influxdata/platform/kv" + platformtesting "github.com/influxdata/platform/testing" +) + +func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func()) { + s, closeFn, err := NewTestKVStore() + if err != nil { + t.Fatalf("failed to create new kv store: %v", err) + } + + err = s.Update(func(tx kv.Tx) error { + b, err := tx.Bucket(f.Bucket) + if err != nil { + return err + } + + for _, p := range f.Pairs { + if err := b.Put(p.Key, p.Value); err != nil { + return err + } + } + + return nil + }) + if err != nil { + t.Fatalf("failed to put keys: %v", err) + } + return s, func() { + closeFn() + } +} + +func TestKVStore(t *testing.T) { + platformtesting.KVStore(initKVStore, t) +} + +func initExampleService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) { + s, closeFn, err := NewTestKVStore() + if err != nil { + t.Fatalf("failed to create new kv store: %v", err) + } + svc := kv.NewExampleService(s, f.IDGenerator) + if err := svc.Initialize(); err != nil { + t.Fatalf("error initializing user service: %v", err) + } + + ctx := context.Background() + for _, u := range f.Users { + if err := svc.PutUser(ctx, u); err != nil { + t.Fatalf("failed to populate users") + } + } + return svc, "", func() { + defer closeFn() + for _, u := range f.Users { + if err := svc.DeleteUser(ctx, u.ID); err != nil { + t.Logf("failed to remove users: %v", err) + } + } + } +} + +func TestExampleService_CreateUser(t *testing.T) { + platformtesting.CreateUser(initExampleService, t) +} + +func TestExampleService_FindUserByID(t *testing.T) { + platformtesting.FindUserByID(initExampleService, t) +} + +func TestExampleService_FindUsers(t *testing.T) { + platformtesting.FindUsers(initExampleService, t) +} + +func TestExampleService_DeleteUser(t *testing.T) { + platformtesting.DeleteUser(initExampleService, t) +} + +func TestExampleService_FindUser(t *testing.T) { + platformtesting.FindUser(initExampleService, t) +} + +func TestExampleService_UpdateUser(t *testing.T) { + platformtesting.UpdateUser(initExampleService, t) +} diff --git a/bolt/user_test.go b/bolt/user_test.go index 6bc7840307..b3c529cc5c 100644 --- a/bolt/user_test.go +++ b/bolt/user_test.go @@ -5,16 +5,17 @@ import ( "testing" "github.com/influxdata/platform" - bolt "github.com/influxdata/platform/bolt" + "github.com/influxdata/platform/bolt" platformtesting "github.com/influxdata/platform/testing" ) func initUserService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) { c, closeFn, err := NewTestClient() if err != nil { - t.Fatalf("failed to create new bolt client: %v", err) + t.Fatalf("failed to create new kv store: %v", err) } c.IDGenerator = f.IDGenerator + ctx := context.Background() for _, u := range f.Users { if err := c.PutUser(ctx, u); err != nil { diff --git a/go.mod b/go.mod index 04b1402e95..33fec377e2 100644 --- a/go.mod +++ b/go.mod @@ -25,8 +25,8 @@ require ( github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e // indirect github.com/cenkalti/backoff v2.0.0+incompatible // indirect github.com/cespare/xxhash v1.1.0 - github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible // indirect - github.com/circonus-labs/circonusllhist v0.1.1 // indirect + github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible // indirect + github.com/circonus-labs/circonusllhist v0.1.3 // indirect github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac // indirect github.com/coreos/bbolt v1.3.1-coreos.6 github.com/davecgh/go-spew v1.1.1 @@ -50,6 +50,7 @@ require ( github.com/gocql/gocql v0.0.0-20181117210152-33c0e89ca93a // indirect github.com/gogo/protobuf v1.1.1 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c github.com/google/go-cmp v0.2.0 github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect @@ -91,7 +92,7 @@ require ( github.com/mattn/go-isatty v0.0.4 github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 - github.com/miekg/dns v1.0.15 // indirect + github.com/miekg/dns v1.1.1 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect diff --git a/go.sum b/go.sum index 9932ecc203..f9f72266f0 100644 --- a/go.sum +++ b/go.sum @@ -66,10 +66,10 @@ github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJk github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible h1:+ZwGzyJGsOwSxIEDDOXzPagR167tQak/1P5wBwH+/dM= -github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.1 h1:MNPpugofgAFpPY/hTULMZIRfN18c5EQc8B8+4oFBx+4= -github.com/circonus-labs/circonusllhist v0.1.1/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible h1:KsuY3ogbxgVv3FNhbLUoT+SE9znoWEUIuChSIT4HukI= +github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac h1:PThQaO4yCvJzJBUW1XoFQxLotWRhvX2fgljJX8yrhFI= github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -137,6 +137,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= @@ -278,8 +280,8 @@ github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 h1:tGfIHhDghvEnneeR github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw= -github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o= +github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= diff --git a/inmem/kv.go b/inmem/kv.go new file mode 100644 index 0000000000..0a20b5af91 --- /dev/null +++ b/inmem/kv.go @@ -0,0 +1,203 @@ +package inmem + +import ( + "bytes" + "context" + "fmt" + "sync" + + "github.com/google/btree" + "github.com/influxdata/platform/kv" +) + +// KVStore is an in memory btree backed kv.Store. +type KVStore struct { + mu sync.RWMutex + buckets map[string]*Bucket +} + +// NewKVStore creates an instance of a KVStore. +func NewKVStore() *KVStore { + return &KVStore{ + buckets: map[string]*Bucket{}, + } +} + +// View opens up a transaction with a read lock. +func (s *KVStore) View(fn func(kv.Tx) error) error { + s.mu.RLock() + defer s.mu.RUnlock() + return fn(&Tx{ + kv: s, + writable: false, + ctx: context.Background(), + }) +} + +// Update opens up a transaction with a write lock. +func (s *KVStore) Update(fn func(kv.Tx) error) error { + s.mu.Lock() + defer s.mu.Unlock() + return fn(&Tx{ + kv: s, + writable: true, + ctx: context.Background(), + }) +} + +// Tx is an in memory transaction. +// TODO: make transactions actually transactional +type Tx struct { + kv *KVStore + writable bool + ctx context.Context +} + +// Context returns the context for the transaction. +func (t *Tx) Context() context.Context { + return t.ctx +} + +// WithContext sets the context for the transaction. +func (t *Tx) WithContext(ctx context.Context) { + t.ctx = ctx +} + +// createBucketIfNotExists creates a btree bucket at the provided key. +func (t *Tx) createBucketIfNotExists(b []byte) (kv.Bucket, error) { + if t.writable { + bkt, ok := t.kv.buckets[string(b)] + if !ok { + bkt = &Bucket{btree.New(2)} + t.kv.buckets[string(b)] = bkt + return &bucket{ + Bucket: bkt, + writable: t.writable, + }, nil + } + + return &bucket{ + Bucket: bkt, + writable: t.writable, + }, nil + } + + return nil, kv.ErrTxNotWritable +} + +// Bucket retrieves the bucket at the provided key. +func (t *Tx) Bucket(b []byte) (kv.Bucket, error) { + bkt, ok := t.kv.buckets[string(b)] + if !ok { + return t.createBucketIfNotExists(b) + } + + return &bucket{ + Bucket: bkt, + writable: t.writable, + }, nil +} + +// Bucket is a btree that implements kv.Bucket. +type Bucket struct { + btree *btree.BTree +} + +type bucket struct { + kv.Bucket + writable bool +} + +// Put wraps the put method of a kv bucket and ensures that the +// bucket is writable. +func (b *bucket) Put(key, value []byte) error { + if b.writable { + return b.Bucket.Put(key, value) + } + return kv.ErrTxNotWritable +} + +// Delete wraps the delete method of a kv bucket and ensures that the +// bucket is writable. +func (b *bucket) Delete(key []byte) error { + if b.writable { + return b.Bucket.Delete(key) + } + return kv.ErrTxNotWritable +} + +type item struct { + key []byte + value []byte +} + +// Less is used to implement btree.Item. +func (i *item) Less(b btree.Item) bool { + j, ok := b.(*item) + if !ok { + return false + } + + return bytes.Compare(i.key, j.key) < 0 +} + +// Get retrieves the value at the provided key. +func (b *Bucket) Get(key []byte) ([]byte, error) { + i := b.btree.Get(&item{key: key}) + + if i == nil { + return nil, kv.ErrKeyNotFound + } + + j, ok := i.(*item) + if !ok { + return nil, fmt.Errorf("error item is type %T not *item", i) + } + + return j.value, nil +} + +// Put sets the key value pair provided. +func (b *Bucket) Put(key []byte, value []byte) error { + _ = b.btree.ReplaceOrInsert(&item{key: key, value: value}) + return nil +} + +// Delete removes the key provided. +func (b *Bucket) Delete(key []byte) error { + _ = b.btree.Delete(&item{key: key}) + return nil +} + +// Cursor creates a static cursor from all entries in the database. +func (b *Bucket) Cursor() (kv.Cursor, error) { + // TODO we should do this by using the Ascend/Descend methods that + // the btree provides. + pairs, err := b.getAll() + if err != nil { + return nil, err + } + + return kv.NewStaticCursor(pairs), nil +} + +func (b *Bucket) getAll() ([]kv.Pair, error) { + pairs := []kv.Pair{} + var err error + b.btree.Ascend(func(i btree.Item) bool { + j, ok := i.(*item) + if !ok { + err = fmt.Errorf("error item is type %T not *item", i) + return false + } + + pairs = append(pairs, kv.Pair{Key: j.key, Value: j.value}) + return true + }) + + if err != nil { + return nil, err + } + + return pairs, nil +} diff --git a/inmem/kv_test.go b/inmem/kv_test.go new file mode 100644 index 0000000000..69eccf8445 --- /dev/null +++ b/inmem/kv_test.go @@ -0,0 +1,64 @@ +package inmem_test + +import ( + "context" + "testing" + + "github.com/influxdata/platform" + "github.com/influxdata/platform/inmem" + "github.com/influxdata/platform/kv" + platformtesting "github.com/influxdata/platform/testing" +) + +func initExampleService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) { + s := inmem.NewKVStore() + svc := kv.NewExampleService(s, f.IDGenerator) + if err := svc.Initialize(); err != nil { + t.Fatalf("error initializing user service: %v", err) + } + + ctx := context.Background() + for _, u := range f.Users { + if err := svc.PutUser(ctx, u); err != nil { + t.Fatalf("failed to populate users") + } + } + return svc, "", func() { + for _, u := range f.Users { + if err := svc.DeleteUser(ctx, u.ID); err != nil { + t.Logf("failed to remove users: %v", err) + } + } + } +} + +func TestExampleService(t *testing.T) { + platformtesting.UserService(initExampleService, t) +} + +func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func()) { + s := inmem.NewKVStore() + + err := s.Update(func(tx kv.Tx) error { + b, err := tx.Bucket(f.Bucket) + if err != nil { + return err + } + + for _, p := range f.Pairs { + if err := b.Put(p.Key, p.Value); err != nil { + return err + } + } + + return nil + }) + if err != nil { + t.Fatalf("failed to put keys: %v", err) + } + return s, func() {} +} + +func TestKVStore(t *testing.T) { + platformtesting.KVStore(initKVStore, t) +} diff --git a/kv/cursor.go b/kv/cursor.go new file mode 100644 index 0000000000..a8a54e57a7 --- /dev/null +++ b/kv/cursor.go @@ -0,0 +1,80 @@ +package kv + +import ( + "bytes" + "sort" +) + +// staticCursor implements the Cursor interface for a slice of +// static key value pairs. +type staticCursor struct { + idx int + pairs []Pair +} + +// Pair is a struct for key value pairs. +type Pair struct { + Key []byte + Value []byte +} + +// NewStaticCursor returns an instance of a StaticCursor. It +// destructively sorts the provided pairs to be in key ascending order. +func NewStaticCursor(pairs []Pair) Cursor { + sort.Slice(pairs, func(i, j int) bool { + return bytes.Compare(pairs[i].Key, pairs[j].Key) < 0 + }) + return &staticCursor{ + pairs: pairs, + } +} + +// Seek searches the slice for the first key with the provided prefix. +func (c *staticCursor) Seek(prefix []byte) ([]byte, []byte) { + // TODO: do binary search for prefix since pairs are ordered. + for i, pair := range c.pairs { + if bytes.HasPrefix(pair.Key, prefix) { + c.idx = i + return pair.Key, pair.Value + } + } + + return nil, nil +} + +func (c *staticCursor) getValueAtIndex(delta int) ([]byte, []byte) { + idx := c.idx + delta + if idx < 0 { + return nil, nil + } + + if idx >= len(c.pairs) { + return nil, nil + } + + c.idx = idx + + pair := c.pairs[c.idx] + + return pair.Key, pair.Value +} + +// First retrieves the first element in the cursor. +func (c *staticCursor) First() ([]byte, []byte) { + return c.getValueAtIndex(-c.idx) +} + +// Last retrieves the last element in the cursor. +func (c *staticCursor) Last() ([]byte, []byte) { + return c.getValueAtIndex(len(c.pairs) - 1 - c.idx) +} + +// Next retrieves the next entry in the cursor. +func (c *staticCursor) Next() ([]byte, []byte) { + return c.getValueAtIndex(1) +} + +// Prev retrieves the previous entry in the cursor. +func (c *staticCursor) Prev() ([]byte, []byte) { + return c.getValueAtIndex(-1) +} diff --git a/kv/cursor_test.go b/kv/cursor_test.go new file mode 100644 index 0000000000..358cd0e5b2 --- /dev/null +++ b/kv/cursor_test.go @@ -0,0 +1,244 @@ +package kv_test + +import ( + "bytes" + "testing" + + "github.com/influxdata/platform/kv" +) + +func TestStaticCursor_First(t *testing.T) { + type args struct { + pairs []kv.Pair + } + type wants struct { + key []byte + val []byte + } + + tests := []struct { + name string + args args + wants wants + }{ + { + name: "nil pairs", + args: args{ + pairs: nil, + }, + wants: wants{}, + }, + { + name: "empty pairs", + args: args{ + pairs: []kv.Pair{}, + }, + wants: wants{}, + }, + { + name: "unsorted pairs", + args: args{ + pairs: []kv.Pair{ + { + Key: []byte("bcd"), + Value: []byte("yoyo"), + }, + { + Key: []byte("abc"), + Value: []byte("oyoy"), + }, + }, + }, + wants: wants{ + key: []byte("abc"), + val: []byte("oyoy"), + }, + }, + { + name: "sorted pairs", + args: args{ + pairs: []kv.Pair{ + { + Key: []byte("abc"), + Value: []byte("oyoy"), + }, + { + Key: []byte("bcd"), + Value: []byte("yoyo"), + }, + }, + }, + wants: wants{ + key: []byte("abc"), + val: []byte("oyoy"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cur := kv.NewStaticCursor(tt.args.pairs) + + key, val := cur.First() + + if want, got := tt.wants.key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + } + + if want, got := tt.wants.val, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + } + }) + } +} + +func TestStaticCursor_Last(t *testing.T) { + type args struct { + pairs []kv.Pair + } + type wants struct { + key []byte + val []byte + } + + tests := []struct { + name string + args args + wants wants + }{ + { + name: "nil pairs", + args: args{ + pairs: nil, + }, + wants: wants{}, + }, + { + name: "empty pairs", + args: args{ + pairs: []kv.Pair{}, + }, + wants: wants{}, + }, + { + name: "unsorted pairs", + args: args{ + pairs: []kv.Pair{ + { + Key: []byte("bcd"), + Value: []byte("yoyo"), + }, + { + Key: []byte("abc"), + Value: []byte("oyoy"), + }, + }, + }, + wants: wants{ + key: []byte("bcd"), + val: []byte("yoyo"), + }, + }, + { + name: "sorted pairs", + args: args{ + pairs: []kv.Pair{ + { + Key: []byte("abc"), + Value: []byte("oyoy"), + }, + { + Key: []byte("bcd"), + Value: []byte("yoyo"), + }, + }, + }, + wants: wants{ + key: []byte("bcd"), + val: []byte("yoyo"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cur := kv.NewStaticCursor(tt.args.pairs) + + key, val := cur.Last() + + if want, got := tt.wants.key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + } + + if want, got := tt.wants.val, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + } + }) + } +} + +func TestStaticCursor_Seek(t *testing.T) { + type args struct { + prefix []byte + pairs []kv.Pair + } + type wants struct { + key []byte + val []byte + } + + tests := []struct { + name string + args args + wants wants + }{ + { + name: "sorted pairs", + args: args{ + prefix: []byte("bc"), + pairs: []kv.Pair{ + { + Key: []byte("abc"), + Value: []byte("oyoy"), + }, + { + Key: []byte("abcd"), + Value: []byte("oyoy"), + }, + { + Key: []byte("bcd"), + Value: []byte("yoyo"), + }, + { + Key: []byte("bcde"), + Value: []byte("yoyo"), + }, + { + Key: []byte("cde"), + Value: []byte("yyoo"), + }, + }, + }, + wants: wants{ + key: []byte("bcd"), + val: []byte("yoyo"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cur := kv.NewStaticCursor(tt.args.pairs) + + key, val := cur.Seek(tt.args.prefix) + + if want, got := tt.wants.key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + } + + if want, got := tt.wants.val, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + } + }) + } +} diff --git a/kv/example.go b/kv/example.go new file mode 100644 index 0000000000..866b2d704e --- /dev/null +++ b/kv/example.go @@ -0,0 +1,436 @@ +// Note: this file is used as a proof of concept for having a generic +// keyvalue store backed by specific implementations of kv.Store. +package kv + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/influxdata/platform" +) + +var ( + exampleBucket = []byte("examplesv1") + exampleIndex = []byte("exampleindexv1") +) + +// ExampleService is an example user like service built on a generic kv store. +type ExampleService struct { + kv Store + idGenerator platform.IDGenerator +} + +// NewExampleService creates an instance of an example service. +func NewExampleService(kv Store, idGen platform.IDGenerator) *ExampleService { + return &ExampleService{ + kv: kv, + idGenerator: idGen, + } +} + +// Initialize creates the buckets for the example service +func (c *ExampleService) Initialize() error { + return c.kv.Update(func(tx Tx) error { + if _, err := tx.Bucket([]byte(exampleBucket)); err != nil { + return err + } + if _, err := tx.Bucket([]byte(exampleIndex)); err != nil { + return err + } + return nil + }) +} + +// FindUserByID retrieves a example by id. +func (c *ExampleService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) { + var u *platform.User + + err := c.kv.View(func(tx Tx) error { + usr, err := c.findUserByID(ctx, tx, id) + if err != nil { + return err + } + u = usr + return nil + }) + + if err != nil { + return nil, &platform.Error{ + Op: platform.OpFindUserByID, + Err: err, + } + } + + return u, nil +} + +func (c *ExampleService) findUserByID(ctx context.Context, tx Tx, id platform.ID) (*platform.User, error) { + encodedID, err := id.Encode() + if err != nil { + return nil, err + } + + b, err := tx.Bucket(exampleBucket) + if err != nil { + return nil, err + } + + v, err := b.Get(encodedID) + if err == ErrKeyNotFound { + return nil, &platform.Error{ + Code: platform.ENotFound, + Msg: "user not found", + } + } + if err != nil { + return nil, err + } + + var u platform.User + if err := json.Unmarshal(v, &u); err != nil { + return nil, err + } + + return &u, nil +} + +// FindUserByName returns a example by name for a particular example. +func (c *ExampleService) FindUserByName(ctx context.Context, n string) (*platform.User, error) { + var u *platform.User + + err := c.kv.View(func(tx Tx) error { + usr, err := c.findUserByName(ctx, tx, n) + if err != nil { + return err + } + u = usr + return nil + }) + + return u, err +} + +func (c *ExampleService) findUserByName(ctx context.Context, tx Tx, n string) (*platform.User, error) { + b, err := tx.Bucket(exampleIndex) + if err != nil { + return nil, err + } + uid, err := b.Get(exampleIndexKey(n)) + if err == ErrKeyNotFound { + return nil, &platform.Error{ + Code: platform.ENotFound, + Msg: "user not found", + Op: platform.OpFindUser, + } + } + if err != nil { + return nil, err + } + + var id platform.ID + if err := id.Decode(uid); err != nil { + return nil, err + } + return c.findUserByID(ctx, tx, id) +} + +// FindUser retrives a example using an arbitrary example filter. +// Filters using ID, or Name should be efficient. +// Other filters will do a linear scan across examples until it finds a match. +func (c *ExampleService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) { + if filter.ID != nil { + return c.FindUserByID(ctx, *filter.ID) + } + + if filter.Name != nil { + return c.FindUserByName(ctx, *filter.Name) + } + + filterFn := filterExamplesFn(filter) + + var u *platform.User + err := c.kv.View(func(tx Tx) error { + return forEachExample(ctx, tx, func(usr *platform.User) bool { + if filterFn(usr) { + u = usr + return false + } + return true + }) + }) + + if err != nil { + return nil, err + } + + if u == nil { + return nil, &platform.Error{ + Code: platform.ENotFound, + Msg: "user not found", + } + } + + return u, nil +} + +func filterExamplesFn(filter platform.UserFilter) func(u *platform.User) bool { + if filter.ID != nil { + return func(u *platform.User) bool { + return u.ID.Valid() && u.ID == *filter.ID + } + } + + if filter.Name != nil { + return func(u *platform.User) bool { + return u.Name == *filter.Name + } + } + + return func(u *platform.User) bool { return true } +} + +// FindUsers retrives all examples that match an arbitrary example filter. +// Filters using ID, or Name should be efficient. +// Other filters will do a linear scan across all examples searching for a match. +func (c *ExampleService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) { + op := platform.OpFindUsers + if filter.ID != nil { + u, err := c.FindUserByID(ctx, *filter.ID) + if err != nil { + return nil, 0, &platform.Error{ + Err: err, + Op: op, + } + } + + return []*platform.User{u}, 1, nil + } + + if filter.Name != nil { + u, err := c.FindUserByName(ctx, *filter.Name) + if err != nil { + return nil, 0, &platform.Error{ + Err: err, + Op: op, + } + } + + return []*platform.User{u}, 1, nil + } + + us := []*platform.User{} + filterFn := filterExamplesFn(filter) + err := c.kv.View(func(tx Tx) error { + return forEachExample(ctx, tx, func(u *platform.User) bool { + if filterFn(u) { + us = append(us, u) + } + return true + }) + }) + + if err != nil { + return nil, 0, err + } + + return us, len(us), nil +} + +// CreateUser creates a platform example and sets b.ID. +func (c *ExampleService) CreateUser(ctx context.Context, u *platform.User) error { + err := c.kv.Update(func(tx Tx) error { + unique := c.uniqueExampleName(ctx, tx, u) + + if !unique { + // TODO: make standard error + return &platform.Error{ + Code: platform.EConflict, + Msg: fmt.Sprintf("user with name %s already exists", u.Name), + } + } + + u.ID = c.idGenerator.ID() + + return c.putUser(ctx, tx, u) + }) + + if err != nil { + return &platform.Error{ + Err: err, + Op: platform.OpCreateUser, + } + } + + return nil +} + +// PutUser will put a example without setting an ID. +func (c *ExampleService) PutUser(ctx context.Context, u *platform.User) error { + return c.kv.Update(func(tx Tx) error { + return c.putUser(ctx, tx, u) + }) +} + +func (c *ExampleService) putUser(ctx context.Context, tx Tx, u *platform.User) error { + v, err := json.Marshal(u) + if err != nil { + return err + } + encodedID, err := u.ID.Encode() + if err != nil { + return err + } + + idx, err := tx.Bucket(exampleIndex) + if err != nil { + return err + } + + if err := idx.Put(exampleIndexKey(u.Name), encodedID); err != nil { + return err + } + + b, err := tx.Bucket(exampleBucket) + if err != nil { + return err + } + + return b.Put(encodedID, v) +} + +func exampleIndexKey(n string) []byte { + return []byte(n) +} + +// forEachExample will iterate through all examples while fn returns true. +func forEachExample(ctx context.Context, tx Tx, fn func(*platform.User) bool) error { + b, err := tx.Bucket(exampleBucket) + if err != nil { + return err + } + + cur, err := b.Cursor() + if err != nil { + return err + } + + for k, v := cur.First(); k != nil; k, v = cur.Next() { + u := &platform.User{} + if err := json.Unmarshal(v, u); err != nil { + return err + } + if !fn(u) { + break + } + } + + return nil +} + +func (c *ExampleService) uniqueExampleName(ctx context.Context, tx Tx, u *platform.User) bool { + idx, err := tx.Bucket(exampleIndex) + if err != nil { + return false + } + + if _, err := idx.Get(exampleIndexKey(u.Name)); err == ErrKeyNotFound { + return true + } + return false +} + +// UpdateUser updates a example according the parameters set on upd. +func (c *ExampleService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) { + var u *platform.User + err := c.kv.Update(func(tx Tx) error { + usr, err := c.updateUser(ctx, tx, id, upd) + if err != nil { + return err + } + u = usr + return nil + }) + + if err != nil { + return nil, &platform.Error{ + Err: err, + Op: platform.OpUpdateUser, + } + } + + return u, nil +} + +func (c *ExampleService) updateUser(ctx context.Context, tx Tx, id platform.ID, upd platform.UserUpdate) (*platform.User, error) { + u, err := c.findUserByID(ctx, tx, id) + if err != nil { + return nil, err + } + + if upd.Name != nil { + // Examples are indexed by name and so the example index must be pruned + // when name is modified. + idx, err := tx.Bucket(exampleIndex) + if err != nil { + return nil, err + } + + if err := idx.Delete(exampleIndexKey(u.Name)); err != nil { + return nil, err + } + u.Name = *upd.Name + } + + if err := c.putUser(ctx, tx, u); err != nil { + return nil, err + } + + return u, nil +} + +// DeleteUser deletes a example and prunes it from the index. +func (c *ExampleService) DeleteUser(ctx context.Context, id platform.ID) error { + err := c.kv.Update(func(tx Tx) error { + return c.deleteUser(ctx, tx, id) + }) + + if err != nil { + return &platform.Error{ + Op: platform.OpDeleteUser, + Err: err, + } + } + + return nil +} + +func (c *ExampleService) deleteUser(ctx context.Context, tx Tx, id platform.ID) error { + u, err := c.findUserByID(ctx, tx, id) + if err != nil { + return err + } + encodedID, err := id.Encode() + if err != nil { + return err + } + + idx, err := tx.Bucket(exampleIndex) + if err != nil { + return err + } + + if err := idx.Delete(exampleIndexKey(u.Name)); err != nil { + return err + } + + b, err := tx.Bucket(exampleBucket) + if err != nil { + return err + } + if err := b.Delete(encodedID); err != nil { + return err + } + + return nil +} diff --git a/kv/store.go b/kv/store.go new file mode 100644 index 0000000000..cab2ce799e --- /dev/null +++ b/kv/store.go @@ -0,0 +1,52 @@ +package kv + +import ( + "context" + "errors" +) + +var ( + // ErrKeyNotFound is the error returned when the key requested is not found. + ErrKeyNotFound = errors.New("key not found") + // ErrTxNotWritable is the error returned when an mutable operation is called during + // a non-writable transaction. + ErrTxNotWritable = errors.New("transaction is not writable") +) + +// Store is an interface for a generic key value store. It is modeled after +// the boltdb database struct. +type Store interface { + // View opens up a transaction that will not write to any data. Implementing interfaces + // should take care to ensure that all view transactions do not mutate any data. + View(func(Tx) error) error + // Update opens up a transaction that will mutate data. + Update(func(Tx) error) error +} + +// Tx is a transaction in the store. +type Tx interface { + Bucket(b []byte) (Bucket, error) + Context() context.Context + WithContext(ctx context.Context) +} + +// Bucket is the abstraction used to perform get/put/delete/get-many operations +// in a key value store. +type Bucket interface { + Get(key []byte) ([]byte, error) + Cursor() (Cursor, error) + // Put should error if the transaction it was called in is not writable. + Put(key, value []byte) error + // Delete should error if the transaction it was called in is not writable. + Delete(key []byte) error +} + +// Cursor is an abstraction for iterating/ranging through data. A concrete implementation +// of a cursor can be found in cursor.go. +type Cursor interface { + Seek(prefix []byte) (k []byte, v []byte) + First() (k []byte, v []byte) + Last() (k []byte, v []byte) + Next() (k []byte, v []byte) + Prev() (k []byte, v []byte) +} diff --git a/testing/kv.go b/testing/kv.go new file mode 100644 index 0000000000..62565dd641 --- /dev/null +++ b/testing/kv.go @@ -0,0 +1,928 @@ +package testing + +import ( + "bytes" + "fmt" + "testing" + "time" + + "github.com/influxdata/platform/kv" +) + +// KVStoreFields are background data that has to be set before +// the test runs. +type KVStoreFields struct { + Bucket []byte + Pairs []kv.Pair +} + +// KVStore tests the key value store contract +func KVStore( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + tests := []struct { + name string + fn func( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, + ) + }{ + { + name: "Get", + fn: KVGet, + }, + { + name: "Put", + fn: KVPut, + }, + { + name: "Delete", + fn: KVDelete, + }, + { + name: "Cursor", + fn: KVCursor, + }, + { + name: "View", + fn: KVView, + }, + { + name: "Update", + fn: KVUpdate, + }, + { + name: "ConcurrentUpdate", + fn: KVConcurrentUpdate, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.fn(init, t) + }) + } +} + +// KVGet tests the get method contract for the key value store. +func KVGet( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + key []byte + } + type wants struct { + err error + val []byte + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "get key", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + }, + wants: wants{ + val: []byte("world"), + }, + }, + { + name: "get missing key", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{}, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + }, + wants: wants{ + err: kv.ErrKeyNotFound, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, close := init(tt.fields, t) + defer close() + + err := s.View(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + val, err := b.Get(tt.args.key) + if (err != nil) != (tt.wants.err != nil) { + t.Errorf("expected error '%v' got '%v'", tt.wants.err, err) + return err + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + return err + } + } + + if want, got := tt.wants.val, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + }) + } +} + +// KVPut tests the get method contract for the key value store. +func KVPut( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + key []byte + val []byte + } + type wants struct { + err error + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "put pair", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{}, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + val: []byte("world"), + }, + wants: wants{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, close := init(tt.fields, t) + defer close() + + err := s.Update(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + { + err := b.Put(tt.args.key, tt.args.val) + if (err != nil) != (tt.wants.err != nil) { + t.Errorf("expected error '%v' got '%v'", tt.wants.err, err) + return err + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + return err + } + } + + val, err := b.Get(tt.args.key) + if err != nil { + t.Errorf("unexpected error retrieving value: %v", err) + return err + } + + if want, got := tt.args.val, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + }) + } +} + +// KVDelete tests the delete method contract for the key value store. +func KVDelete( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + key []byte + } + type wants struct { + err error + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "delete key", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + }, + wants: wants{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, close := init(tt.fields, t) + defer close() + + err := s.Update(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + { + err := b.Delete(tt.args.key) + if (err != nil) != (tt.wants.err != nil) { + t.Errorf("expected error '%v' got '%v'", tt.wants.err, err) + return err + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + return err + } + } + + if _, err := b.Get(tt.args.key); err != kv.ErrKeyNotFound { + t.Errorf("expected key not found error got %v", err) + return err + } + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + }) + } +} + +// KVCursor tests the cursor contract for the key value store. +func KVCursor( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + seek []byte + } + type wants struct { + err error + first kv.Pair + last kv.Pair + seek kv.Pair + next kv.Pair + prev kv.Pair + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "basic cursor", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("a"), + Value: []byte("1"), + }, + { + Key: []byte("ab"), + Value: []byte("2"), + }, + { + Key: []byte("abc"), + Value: []byte("3"), + }, + { + Key: []byte("abcd"), + Value: []byte("4"), + }, + { + Key: []byte("abcde"), + Value: []byte("5"), + }, + { + Key: []byte("bcd"), + Value: []byte("6"), + }, + { + Key: []byte("cd"), + Value: []byte("7"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + seek: []byte("abc"), + }, + wants: wants{ + first: kv.Pair{ + Key: []byte("a"), + Value: []byte("1"), + }, + last: kv.Pair{ + Key: []byte("cd"), + Value: []byte("7"), + }, + seek: kv.Pair{ + Key: []byte("abc"), + Value: []byte("3"), + }, + next: kv.Pair{ + Key: []byte("abcd"), + Value: []byte("4"), + }, + prev: kv.Pair{ + Key: []byte("abc"), + Value: []byte("3"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, close := init(tt.fields, t) + defer close() + + err := s.View(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + cur, err := b.Cursor() + if (err != nil) != (tt.wants.err != nil) { + t.Errorf("expected error '%v' got '%v'", tt.wants.err, err) + return err + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + return err + } + } + + { + key, val := cur.First() + if want, got := tt.wants.first.Key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + return err + } + + if want, got := tt.wants.first.Value, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + } + + { + key, val := cur.Last() + if want, got := tt.wants.last.Key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + return err + } + + if want, got := tt.wants.last.Value, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + } + + { + key, val := cur.Seek(tt.args.seek) + if want, got := tt.wants.seek.Key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + return err + } + + if want, got := tt.wants.seek.Value, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + } + + { + key, val := cur.Next() + if want, got := tt.wants.next.Key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + return err + } + + if want, got := tt.wants.next.Value, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + } + + { + key, val := cur.Prev() + if want, got := tt.wants.prev.Key, key; !bytes.Equal(want, got) { + t.Errorf("exptected to get key %s got %s", string(want), string(got)) + return err + } + + if want, got := tt.wants.prev.Value, val; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + }) + } +} + +// KVView tests the view method contract for the key value store. +func KVView( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + key []byte + // If len(value) == 0 the test will not attempt a put + value []byte + // If true, the test will attempt to delete the provided key + delete bool + } + type wants struct { + value []byte + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "basic view", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("cruel world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + }, + wants: wants{ + value: []byte("cruel world"), + }, + }, + { + name: "basic view with delete", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("cruel world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + delete: true, + }, + wants: wants{ + value: []byte("cruel world"), + }, + }, + { + name: "basic view with put", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("cruel world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + value: []byte("world"), + delete: true, + }, + wants: wants{ + value: []byte("cruel world"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, close := init(tt.fields, t) + defer close() + + err := s.View(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + if len(tt.args.value) != 0 { + err := b.Put(tt.args.key, tt.args.value) + if err == nil { + return fmt.Errorf("expected transaction to fail") + } + if err != kv.ErrTxNotWritable { + return err + } + return nil + } + + value, err := b.Get(tt.args.key) + if err != nil { + return err + } + + if want, got := tt.wants.value, value; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + + if tt.args.delete { + err := b.Delete(tt.args.key) + if err == nil { + return fmt.Errorf("expected transaction to fail") + } + if err != kv.ErrTxNotWritable { + return err + } + return nil + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + }) + } +} + +// KVUpdate tests the update method contract for the key value store. +func KVUpdate( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + key []byte + value []byte + delete bool + } + type wants struct { + value []byte + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "basic update", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("cruel world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + value: []byte("world"), + }, + wants: wants{ + value: []byte("world"), + }, + }, + { + name: "basic update with delete", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("cruel world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + value: []byte("world"), + delete: true, + }, + wants: wants{}, + }, + // TODO: add case with failed update transaction that doesn't apply all of the changes. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, close := init(tt.fields, t) + defer close() + + { + err := s.Update(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + if len(tt.args.value) != 0 { + err := b.Put(tt.args.key, tt.args.value) + if err != nil { + return err + } + } + + if tt.args.delete { + err := b.Delete(tt.args.key) + if err != nil { + return err + } + } + + value, err := b.Get(tt.args.key) + if tt.args.delete { + if err != kv.ErrKeyNotFound { + return fmt.Errorf("expected key not found") + } + return nil + } else if err != nil { + return err + } + + if want, got := tt.wants.value, value; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + + return nil + }) + + if err != nil { + t.Fatalf("error during update transaction: %v", err) + } + } + + { + err := s.View(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + value, err := b.Get(tt.args.key) + if tt.args.delete { + if err != kv.ErrKeyNotFound { + return fmt.Errorf("expected key not found") + } + } else if err != nil { + return err + } + + if want, got := tt.wants.value, value; !bytes.Equal(want, got) { + t.Errorf("exptected to get value %s got %s", string(want), string(got)) + return err + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + } + }) + } +} + +// KVConcurrentUpdate tests concurrent calls to update. +func KVConcurrentUpdate( + init func(KVStoreFields, *testing.T) (kv.Store, func()), + t *testing.T, +) { + type args struct { + bucket []byte + key []byte + valueA []byte + valueB []byte + } + type wants struct { + value []byte + } + + tests := []struct { + name string + fields KVStoreFields + args args + wants wants + }{ + { + name: "basic concurrent update", + fields: KVStoreFields{ + Bucket: []byte("bucket"), + Pairs: []kv.Pair{ + { + Key: []byte("hello"), + Value: []byte("cruel world"), + }, + }, + }, + args: args{ + bucket: []byte("bucket"), + key: []byte("hello"), + valueA: []byte("world"), + valueB: []byte("darkness my new friend"), + }, + wants: wants{ + value: []byte("darkness my new friend"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, closeFn := init(tt.fields, t) + defer closeFn() + + errCh := make(chan error) + var fn = func(v []byte) { + err := s.Update(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + return err + } + + if err := b.Put(tt.args.key, v); err != nil { + return err + } + + return nil + }) + + if err != nil { + errCh <- fmt.Errorf("error during update transaction: %v", err) + } else { + errCh <- nil + } + } + go fn(tt.args.valueA) + // To ensure that a is scheduled before b + time.Sleep(time.Millisecond) + go fn(tt.args.valueB) + + count := 0 + for err := range errCh { + count++ + if err != nil { + t.Fatal(err) + } + if count == 2 { + break + } + } + + close(errCh) + + { + err := s.View(func(tx kv.Tx) error { + b, err := tx.Bucket(tt.args.bucket) + if err != nil { + t.Errorf("unexpected error retrieving bucket: %v", err) + return err + } + + deadline := time.Now().Add(1 * time.Second) + var returnErr error + for { + if time.Now().After(deadline) { + break + } + + value, err := b.Get(tt.args.key) + if err != nil { + return err + } + + if want, got := tt.wants.value, value; !bytes.Equal(want, got) { + returnErr = fmt.Errorf("exptected to get value %s got %s", string(want), string(got)) + } else { + returnErr = nil + break + } + } + + if returnErr != nil { + return returnErr + } + + return nil + }) + + if err != nil { + t.Fatalf("error during view transaction: %v", err) + } + } + }) + } +} diff --git a/user.go b/user.go index a2acbef52c..9a55f4d433 100644 --- a/user.go +++ b/user.go @@ -1,6 +1,8 @@ package platform -import "context" +import ( + "context" +) // User is a user. 🎉 type User struct { From f4a291d1fff88cebb76a9258d6bc6031e5a49c2e Mon Sep 17 00:00:00 2001 From: Palak Bhojani Date: Tue, 18 Dec 2018 13:21:30 -0800 Subject: [PATCH 07/14] Telegraf plugins alphabetized in the sidebar --- ui/src/onboarding/reducers/dataLoaders.test.ts | 4 ++-- ui/src/onboarding/reducers/dataLoaders.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/src/onboarding/reducers/dataLoaders.test.ts b/ui/src/onboarding/reducers/dataLoaders.test.ts index e443f0febc..9647cf7288 100644 --- a/ui/src/onboarding/reducers/dataLoaders.test.ts +++ b/ui/src/onboarding/reducers/dataLoaders.test.ts @@ -361,9 +361,9 @@ describe('dataLoader reducer', () => { const expected = { ...INITIAL_STATE, telegrafPlugins: [ - redisTelegrafPlugin, - diskTelegrafPlugin, cpuTelegrafPlugin, + diskTelegrafPlugin, + redisTelegrafPlugin, ], } diff --git a/ui/src/onboarding/reducers/dataLoaders.ts b/ui/src/onboarding/reducers/dataLoaders.ts index 976fd8f2c7..0b5bd02816 100644 --- a/ui/src/onboarding/reducers/dataLoaders.ts +++ b/ui/src/onboarding/reducers/dataLoaders.ts @@ -78,9 +78,12 @@ export default (state = INITIAL_STATE, action: Action): DataLoadersState => { case 'ADD_TELEGRAF_PLUGINS': return { ...state, - telegrafPlugins: _.uniqBy( - [...state.telegrafPlugins, ...action.payload.telegrafPlugins], - 'name' + telegrafPlugins: _.sortBy( + _.uniqBy( + [...state.telegrafPlugins, ...action.payload.telegrafPlugins], + 'name' + ), + ['name'] ), } case 'UPDATE_TELEGRAF_PLUGIN': From 86b10a75c9f747b409244ffd7efdf38ffe2645fb Mon Sep 17 00:00:00 2001 From: "Christopher M. Wolff" Date: Tue, 18 Dec 2018 13:37:25 -0800 Subject: [PATCH 08/14] fix(http): make query service look for platform.Error in response (#2027) --- http/query_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/query_service.go b/http/query_service.go index 4f8676f501..c7a4757332 100644 --- a/http/query_service.go +++ b/http/query_service.go @@ -171,7 +171,7 @@ func (s *QueryService) Query(ctx context.Context, req *query.Request) (flux.Resu if err != nil { return nil, err } - if err := CheckError(resp); err != nil { + if err := CheckError(resp, true); err != nil { return nil, err } From aeb279228dae6a5973bd905b6f4986a53d2a0cf8 Mon Sep 17 00:00:00 2001 From: Palak Bhojani Date: Tue, 18 Dec 2018 13:55:42 -0800 Subject: [PATCH 09/14] Remove id prop from input component --- ui/src/clockface/components/inputs/Input.tsx | 1 - .../components/account/__snapshots__/Settings.test.tsx.snap | 2 -- .../me/components/account/__snapshots__/Tokens.test.tsx.snap | 2 -- .../components/__snapshots__/AdminStep.test.tsx.snap | 5 ----- .../components/configureStep/streaming/MultipleInput.tsx | 1 - 5 files changed, 11 deletions(-) diff --git a/ui/src/clockface/components/inputs/Input.tsx b/ui/src/clockface/components/inputs/Input.tsx index d2a10267ff..fd7e04137a 100644 --- a/ui/src/clockface/components/inputs/Input.tsx +++ b/ui/src/clockface/components/inputs/Input.tsx @@ -53,7 +53,6 @@ interface Props { class Input extends Component { public static defaultProps: Partial = { - id: '', name: '', value: '', placeholder: '', diff --git a/ui/src/me/components/account/__snapshots__/Settings.test.tsx.snap b/ui/src/me/components/account/__snapshots__/Settings.test.tsx.snap index a631e74267..04bee42934 100644 --- a/ui/src/me/components/account/__snapshots__/Settings.test.tsx.snap +++ b/ui/src/me/components/account/__snapshots__/Settings.test.tsx.snap @@ -93,7 +93,6 @@ exports[`Account rendering renders! 1`] = ` autocomplete="off" dataTest="nameInput" disabledTitleText="This input is disabled" - id="" name="" onChange={[Function]} placeholder="" @@ -117,7 +116,6 @@ exports[`Account rendering renders! 1`] = ` className="input-field" data-test="nameInput" disabled={true} - id="" name="" onChange={[Function]} placeholder="" diff --git a/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap b/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap index e634bc6511..6622ea397f 100644 --- a/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap +++ b/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap @@ -16,7 +16,6 @@ exports[`Account rendering renders! 1`] = ` autoFocus={false} autocomplete="off" disabledTitleText="This input is disabled" - id="" name="" onChange={[Function]} placeholder="Filter tokens by column" @@ -40,7 +39,6 @@ exports[`Account rendering renders! 1`] = ` autoFocus={false} className="input-field" disabled={false} - id="" name="" onChange={[Function]} placeholder="Filter tokens by column" diff --git a/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap b/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap index 637225cfb0..9f8d225c6d 100644 --- a/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap +++ b/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap @@ -29,7 +29,6 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Admin username has been set" icon="checkmark" - id="" name="" onChange={[Function]} placeholder="" @@ -52,7 +51,6 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Admin password has been set" icon="checkmark" - id="" name="" onChange={[Function]} placeholder="" @@ -75,7 +73,6 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Admin password has been set" icon="checkmark" - id="" name="" onChange={[Function]} placeholder="" @@ -99,7 +96,6 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Default organization name has been set" icon="checkmark" - id="" name="" onChange={[Function]} placeholder="Your organization is where everything you create lives" @@ -122,7 +118,6 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` autocomplete="off" disabledTitleText="Default bucket name has been set" icon="checkmark" - id="" name="" onChange={[Function]} placeholder="Your bucket is where you will store all your data" diff --git a/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx index 36e92373d0..35bcbff199 100644 --- a/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx +++ b/ui/src/onboarding/components/configureStep/streaming/MultipleInput.tsx @@ -56,7 +56,6 @@ class MultipleInput extends PureComponent { Date: Tue, 18 Dec 2018 14:49:22 -0800 Subject: [PATCH 10/14] chore(http): add missing labels documentation --- http/label_service.go | 10 --- http/swagger.yml | 141 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 10 deletions(-) diff --git a/http/label_service.go b/http/label_service.go index c418bc4a52..740874bb34 100644 --- a/http/label_service.go +++ b/http/label_service.go @@ -1,13 +1,3 @@ -// responses from /labels should look like: -// { -// labels: [ -// "foo", -// "bar" -// ] -// } -// -// this list (under key "labels") should be returned with any labelled resource that is requested via other endpoints - package http import ( diff --git a/http/swagger.yml b/http/swagger.yml index 5ff7575a86..2953de5b5e 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -2650,6 +2650,147 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + '/buckets/{bucketID}/labels': + get: + tags: + - Buckets + summary: list all labels for a bucket + parameters: + - in: path + name: bucketID + schema: + type: string + required: true + description: ID of the bucket + responses: + '200': + description: a list of all labels for a bucket + content: + application/json: + schema: + type: object + properties: + labels: + $ref: "#/components/schemas/Labels" + links: + $ref: "#/components/schemas/Links" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + tags: + - Buckets + summary: add a label to a bucket + parameters: + - in: path + name: bucketID + schema: + type: string + required: true + description: ID of the bucket + requestBody: + description: label to add + required: true + content: + application/json: + schema: + type: object + properties: + label: + type: string + responses: + '200': + description: a list of all labels for a bucket + content: + application/json: + schema: + type: object + properties: + labels: + $ref: "#/components/schemas/Labels" + links: + $ref: "#/components/schemas/Links" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + '/buckets/{bucketID}/labels/{label}': + delete: + tags: + - Buckets + summary: delete a label from a bucket + parameters: + - in: path + name: bucketID + schema: + type: string + required: true + description: ID of the bucket + - in: path + name: label + schema: + type: string + required: true + description: the label name + responses: + '204': + description: delete has been accepted + '404': + description: bucket not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + patch: + tags: + - Buckets + summary: update a label from a bucket + parameters: + - in: path + name: bucketID + schema: + type: string + required: true + description: ID of the bucket + - in: path + name: label + schema: + type: string + required: true + description: the label name + requestBody: + description: label update to apply + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Label" + responses: + '200': + description: updated successfully + '404': + description: bucket not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" '/buckets/{bucketID}/members': get: tags: From d817ff45076f8e088774acb02c82b8b1d8e6f7f7 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Tue, 18 Dec 2018 13:58:26 -0800 Subject: [PATCH 11/14] fix(ui/dataLoaders): Separate the error state from the data not found state when listening for data --- .../verifyStep/ConnectionInformation.test.tsx | 19 ++++++---- .../verifyStep/ConnectionInformation.tsx | 35 ++++++++++++------- .../components/verifyStep/DataListening.tsx | 26 +++++++------- .../components/verifyStep/DataStreaming.tsx | 12 +------ .../components/verifyStep/VerifyDataStep.tsx | 1 - .../verifyStep/VerifyDataSwitcher.tsx | 3 -- .../ConnectionInformation.test.tsx.snap | 15 +++++++- 7 files changed, 63 insertions(+), 48 deletions(-) diff --git a/ui/src/onboarding/components/verifyStep/ConnectionInformation.test.tsx b/ui/src/onboarding/components/verifyStep/ConnectionInformation.test.tsx index aefee257e1..6433fccd97 100644 --- a/ui/src/onboarding/components/verifyStep/ConnectionInformation.test.tsx +++ b/ui/src/onboarding/components/verifyStep/ConnectionInformation.test.tsx @@ -3,14 +3,15 @@ import React from 'react' import {shallow} from 'enzyme' // Components -import ConnectionInformation from 'src/onboarding/components/verifyStep/ConnectionInformation' +import ConnectionInformation, { + LoadingState, +} from 'src/onboarding/components/verifyStep/ConnectionInformation' // Types -import {RemoteDataState} from 'src/types' const setup = (override = {}) => { const props = { - loading: RemoteDataState.NotStarted, + loading: LoadingState.NotStarted, bucket: 'defbuck', countDownSeconds: 60, ...override, @@ -29,19 +30,25 @@ describe('Onboarding.Components.ConnectionInformation', () => { }) it('matches snapshot if loading', () => { - const {wrapper} = setup({loading: RemoteDataState.Loading}) + const {wrapper} = setup({loading: LoadingState.Loading}) expect(wrapper).toMatchSnapshot() }) it('matches snapshot if success', () => { - const {wrapper} = setup({loading: RemoteDataState.Done}) + const {wrapper} = setup({loading: LoadingState.Done}) + + expect(wrapper).toMatchSnapshot() + }) + + it('matches snapshot if no data is found', () => { + const {wrapper} = setup({loading: LoadingState.NotFound}) expect(wrapper).toMatchSnapshot() }) it('matches snapshot if error', () => { - const {wrapper} = setup({loading: RemoteDataState.Error}) + const {wrapper} = setup({loading: LoadingState.Error}) expect(wrapper).toMatchSnapshot() }) diff --git a/ui/src/onboarding/components/verifyStep/ConnectionInformation.tsx b/ui/src/onboarding/components/verifyStep/ConnectionInformation.tsx index 993b4dfb79..91a2ccc472 100644 --- a/ui/src/onboarding/components/verifyStep/ConnectionInformation.tsx +++ b/ui/src/onboarding/components/verifyStep/ConnectionInformation.tsx @@ -5,11 +5,16 @@ import _ from 'lodash' // Decorator import {ErrorHandling} from 'src/shared/decorators/errors' -// Types -import {RemoteDataState} from 'src/types' +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + NotFound = 'NotFound', + Error = 'Error', +} export interface Props { - loading: RemoteDataState + loading: LoadingState bucket: string countDownSeconds: number } @@ -29,33 +34,37 @@ class ListeningResults extends PureComponent { private get className(): string { switch (this.props.loading) { - case RemoteDataState.Loading: + case LoadingState.Loading: return 'loading' - case RemoteDataState.Done: + case LoadingState.Done: return 'success' - case RemoteDataState.Error: + case LoadingState.NotFound: + case LoadingState.Error: return 'error' } } private get header(): string { switch (this.props.loading) { - case RemoteDataState.Loading: + case LoadingState.Loading: return 'Awaiting Connection...' - case RemoteDataState.Done: + case LoadingState.Done: return 'Connection Found!' - case RemoteDataState.Error: - return 'Connection Not Found' + case LoadingState.NotFound: + return 'Data Not Found' + case LoadingState.Error: + return 'Error Listening for Data' } } private get additionalText(): string { switch (this.props.loading) { - case RemoteDataState.Loading: + case LoadingState.Loading: return `Timeout in ${this.props.countDownSeconds} seconds` - case RemoteDataState.Done: + case LoadingState.Done: return `${this.props.bucket} is receiving data loud and clear!` - case RemoteDataState.Error: + case LoadingState.NotFound: + case LoadingState.Error: return 'Check config and try again' } } diff --git a/ui/src/onboarding/components/verifyStep/DataListening.tsx b/ui/src/onboarding/components/verifyStep/DataListening.tsx index b085cc56d9..69b944bf69 100644 --- a/ui/src/onboarding/components/verifyStep/DataListening.tsx +++ b/ui/src/onboarding/components/verifyStep/DataListening.tsx @@ -13,13 +13,14 @@ import { ComponentSize, ComponentStatus, } from 'src/clockface' -import ConnectionInformation from 'src/onboarding/components/verifyStep/ConnectionInformation' +import ConnectionInformation, { + LoadingState, +} from 'src/onboarding/components/verifyStep/ConnectionInformation' // Constants import {StepStatus} from 'src/clockface/constants/wizard' // Types -import {RemoteDataState} from 'src/types' import {InfluxLanguage} from 'src/types/v2/dashboards' export interface Props { @@ -29,7 +30,7 @@ export interface Props { } interface State { - loading: RemoteDataState + loading: LoadingState timePassedInSeconds: number secondsLeft: number } @@ -49,7 +50,7 @@ class DataListening extends PureComponent { super(props) this.state = { - loading: RemoteDataState.NotStarted, + loading: LoadingState.NotStarted, timePassedInSeconds: 0, secondsLeft: SECONDS, } @@ -76,7 +77,7 @@ class DataListening extends PureComponent { private get connectionInfo(): JSX.Element { const {loading} = this.state - if (loading === RemoteDataState.NotStarted) { + if (loading === LoadingState.NotStarted) { return } @@ -88,13 +89,11 @@ class DataListening extends PureComponent { /> ) } + private get listenButton(): JSX.Element { const {loading} = this.state - if ( - loading === RemoteDataState.Loading || - loading === RemoteDataState.Done - ) { + if (loading === LoadingState.Loading || loading === LoadingState.Done) { return } @@ -112,7 +111,7 @@ class DataListening extends PureComponent { private handleClick = (): void => { this.startTimer() - this.setState({loading: RemoteDataState.Loading}) + this.setState({loading: LoadingState.Loading}) this.startTime = Number(new Date()) this.checkForData() } @@ -135,19 +134,19 @@ class DataListening extends PureComponent { rowCount = response.rowCount timePassed = Number(new Date()) - this.startTime } catch (err) { - this.setState({loading: RemoteDataState.Error}) + this.setState({loading: LoadingState.Error}) onSetStepStatus(stepIndex, StepStatus.Incomplete) return } if (rowCount > 1) { - this.setState({loading: RemoteDataState.Done}) + this.setState({loading: LoadingState.Done}) onSetStepStatus(stepIndex, StepStatus.Complete) return } if (timePassed >= MINUTE || secondsLeft <= 0) { - this.setState({loading: RemoteDataState.Error}) + this.setState({loading: LoadingState.NotFound}) onSetStepStatus(stepIndex, StepStatus.Incomplete) return } @@ -159,6 +158,7 @@ class DataListening extends PureComponent { this.timer = setInterval(this.countDown, TIMER_WAIT) } + private countDown = () => { const {secondsLeft} = this.state const secs = secondsLeft - 1 diff --git a/ui/src/onboarding/components/verifyStep/DataStreaming.tsx b/ui/src/onboarding/components/verifyStep/DataStreaming.tsx index e445260020..adaf313286 100644 --- a/ui/src/onboarding/components/verifyStep/DataStreaming.tsx +++ b/ui/src/onboarding/components/verifyStep/DataStreaming.tsx @@ -5,7 +5,6 @@ import _ from 'lodash' // Components import TelegrafInstructions from 'src/onboarding/components/verifyStep/TelegrafInstructions' import CreateOrUpdateConfig from 'src/onboarding/components/verifyStep/CreateOrUpdateConfig' -import FetchAuthToken from 'src/onboarding/components/verifyStep/FetchAuthToken' import DataListening from 'src/onboarding/components/verifyStep/DataListening' // Actions @@ -21,7 +20,6 @@ interface Props { bucket: string org: string configID: string - username: string stepIndex: number authToken: string onSetStepStatus: (index: number, status: StepStatus) => void @@ -34,7 +32,6 @@ class DataStreaming extends PureComponent { const { authToken, org, - username, configID, onSaveTelegrafConfig, onSetStepStatus, @@ -50,14 +47,7 @@ class DataStreaming extends PureComponent { onSaveTelegrafConfig={onSaveTelegrafConfig} > {() => ( - - {authToken => ( - - )} - + )} diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx index 3ea0f2a349..fe1ec9739f 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataStep.tsx @@ -54,7 +54,6 @@ class VerifyDataStep extends PureComponent { authToken={authToken} onSaveTelegrafConfig={onSaveTelegrafConfig} org={_.get(setupParams, 'org', '')} - username={_.get(setupParams, 'username', '')} bucket={_.get(setupParams, 'bucket', '')} onSetStepStatus={onSetStepStatus} stepIndex={stepIndex} diff --git a/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx b/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx index a29748fbe2..b03d1321e0 100644 --- a/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx +++ b/ui/src/onboarding/components/verifyStep/VerifyDataSwitcher.tsx @@ -17,7 +17,6 @@ import {DataLoaderType} from 'src/types/v2/dataLoaders' export interface Props { type: DataLoaderType org: string - username: string bucket: string stepIndex: number authToken: string @@ -31,7 +30,6 @@ class VerifyDataSwitcher extends PureComponent { public render() { const { org, - username, bucket, type, stepIndex, @@ -48,7 +46,6 @@ class VerifyDataSwitcher extends PureComponent { org={org} configID={telegrafConfigID} authToken={authToken} - username={username} bucket={bucket} onSetStepStatus={onSetStepStatus} onSaveTelegrafConfig={onSaveTelegrafConfig} diff --git a/ui/src/onboarding/components/verifyStep/__snapshots__/ConnectionInformation.test.tsx.snap b/ui/src/onboarding/components/verifyStep/__snapshots__/ConnectionInformation.test.tsx.snap index 4d1b9dacf2..0c8c3e2dbb 100644 --- a/ui/src/onboarding/components/verifyStep/__snapshots__/ConnectionInformation.test.tsx.snap +++ b/ui/src/onboarding/components/verifyStep/__snapshots__/ConnectionInformation.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Onboarding.Components.ConnectionInformation matches snapshot if error 1

- Connection Not Found + Error Listening for Data

Check config and try again @@ -26,6 +26,19 @@ exports[`Onboarding.Components.ConnectionInformation matches snapshot if loading `; +exports[`Onboarding.Components.ConnectionInformation matches snapshot if no data is found 1`] = ` + +

+ Data Not Found +

+

+ Check config and try again +

+ +`; + exports[`Onboarding.Components.ConnectionInformation matches snapshot if success 1`] = `

Date: Tue, 18 Dec 2018 16:15:08 -0800 Subject: [PATCH 12/14] fix swagger indentation --- http/swagger.yml | 140 +++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/http/swagger.yml b/http/swagger.yml index 2953de5b5e..b285f68188 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -2752,45 +2752,45 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - patch: - tags: - - Buckets - summary: update a label from a bucket - parameters: - - in: path - name: bucketID - schema: - type: string - required: true - description: ID of the bucket - - in: path - name: label - schema: - type: string - required: true - description: the label name - requestBody: - description: label update to apply + patch: + tags: + - Buckets + summary: update a label from a bucket + parameters: + - in: path + name: bucketID + schema: + type: string required: true + description: ID of the bucket + - in: path + name: label + schema: + type: string + required: true + description: the label name + requestBody: + description: label update to apply + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Label" + responses: + '200': + description: updated successfully + '404': + description: bucket not found content: application/json: schema: - $ref: "#/components/schemas/Label" - responses: - '200': - description: updated successfully - '404': - description: bucket not found - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" + $ref: "#/components/schemas/Error" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" '/buckets/{bucketID}/members': get: tags: @@ -3189,45 +3189,45 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - patch: - tags: - - Organizations - summary: update a label from an organization - parameters: - - in: path - name: orgID - schema: - type: string - required: true - description: ID of the organization - - in: path - name: label - schema: - type: string - required: true - description: the label name - requestBody: - description: label update to apply + patch: + tags: + - Organizations + summary: update a label from an organization + parameters: + - in: path + name: orgID + schema: + type: string required: true + description: ID of the organization + - in: path + name: label + schema: + type: string + required: true + description: the label name + requestBody: + description: label update to apply + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Label" + responses: + '200': + description: updated successfully + '404': + description: organization not found content: application/json: schema: - $ref: "#/components/schemas/Label" - responses: - '200': - description: updated successfully - '404': - description: organization not found - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" + $ref: "#/components/schemas/Error" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" '/orgs/{orgID}/secrets': get: tags: From ef6321a7086898ac8c8567a76e41f2ee495a8e91 Mon Sep 17 00:00:00 2001 From: Daniel Campbell Date: Tue, 18 Dec 2018 18:24:09 -0800 Subject: [PATCH 13/14] Data loader button bar (#2039) * Add Skip to Verify on streaming data steps, style wizard button bar * Add tests, update snapshots --- ui/src/onboarding/OnboardingWizard.scss | 26 ++++----- ui/src/onboarding/components/AdminStep.tsx | 34 ++++++------ .../onboarding/components/CompletionStep.tsx | 6 +-- ui/src/onboarding/components/OtherStep.tsx | 30 ++++++----- .../__snapshots__/AdminStep.test.tsx.snap | 54 ++++++++++--------- .../CompletionStep.test.tsx.snap | 6 +-- .../ConfigureDataSourceStep.test.tsx | 50 +++++++++++++++++ .../configureStep/ConfigureDataSourceStep.tsx | 27 ++++++---- .../lineProtocol/LineProtocolTabs.tsx | 2 +- .../LineProtocolTabs.test.tsx.snap | 2 +- .../SelectDataSourceStep.test.tsx | 29 +++++++++- .../selectionStep/SelectDataSourceStep.tsx | 34 ++++++++---- .../components/verifyStep/VerifyDataStep.tsx | 7 +-- 13 files changed, 208 insertions(+), 99 deletions(-) diff --git a/ui/src/onboarding/OnboardingWizard.scss b/ui/src/onboarding/OnboardingWizard.scss index dc2a45bd87..4092543379 100644 --- a/ui/src/onboarding/OnboardingWizard.scss +++ b/ui/src/onboarding/OnboardingWizard.scss @@ -35,12 +35,24 @@ flex: 1 0 calc(100% - 240px); } -.wizard-button-bar { +.wizard--button-container { + position: absolute; + bottom: 30px; + left: 30px; + right: 30px; + + .wizard--skip-button { + position: absolute; + right: 0; + } +} + +.wizard--button-bar { display: inline-flex; flex-shrink: 0; - margin: 10px auto; position: relative; min-width: 100%; + width: 100%; justify-content: center; align-items: center; height: auto; @@ -53,16 +65,6 @@ } } -.wizard-button-container { - display: inline-flex; - flex-direction: column; - align-items: center; - - .button { - width: 50%; - } -} - .splash-logo { background-size: 100% 100%; background-position: center center; diff --git a/ui/src/onboarding/components/AdminStep.tsx b/ui/src/onboarding/components/AdminStep.tsx index 0319afdeae..be1d4bc1d1 100644 --- a/ui/src/onboarding/components/AdminStep.tsx +++ b/ui/src/onboarding/components/AdminStep.tsx @@ -144,23 +144,25 @@ class AdminStep extends PureComponent { disabledTitleText="Default bucket name has been set" /> +
+
+
+
-
-

) } diff --git a/ui/src/onboarding/components/CompletionStep.tsx b/ui/src/onboarding/components/CompletionStep.tsx index c48b0c549c..119771175f 100644 --- a/ui/src/onboarding/components/CompletionStep.tsx +++ b/ui/src/onboarding/components/CompletionStep.tsx @@ -26,17 +26,17 @@ class CompletionStep extends PureComponent {

Setup Complete!

-
+
diff --git a/ui/src/onboarding/components/OtherStep.tsx b/ui/src/onboarding/components/OtherStep.tsx index 9f2fb2bbd1..84575ea454 100644 --- a/ui/src/onboarding/components/OtherStep.tsx +++ b/ui/src/onboarding/components/OtherStep.tsx @@ -25,20 +25,22 @@ class OtherStep extends PureComponent {

This is Another Step

Import data here
-
-
) diff --git a/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap b/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap index 9f8d225c6d..90e6d74e98 100644 --- a/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap +++ b/ui/src/onboarding/components/__snapshots__/AdminStep.test.tsx.snap @@ -128,31 +128,35 @@ exports[`Onboarding.Components.AdminStep renders 1`] = ` value="" /> +
+
+
+
-
-
`; diff --git a/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap b/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap index 4344950f9f..5622043dee 100644 --- a/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap +++ b/ui/src/onboarding/components/__snapshots__/CompletionStep.test.tsx.snap @@ -16,14 +16,14 @@ exports[`Onboarding.Components.CompletionStep renders 1`] = ` className="wizard-step--sub-title" />
+ data-test="skip" + /> ) } private jumpToCompletionStep = () => { - const {onSetCurrentStepIndex, stepStatuses} = this.props + const {onSetCurrentStepIndex, stepStatuses, type} = this.props this.handleSetStepStatus() - onSetCurrentStepIndex(stepStatuses.length - 1) + + if (type === DataLoaderType.Streaming) { + onSetCurrentStepIndex(stepStatuses.length - 2) + } else { + onSetCurrentStepIndex(stepStatuses.length - 1) + } } private handleNext = async () => { diff --git a/ui/src/onboarding/components/configureStep/lineProtocol/LineProtocolTabs.tsx b/ui/src/onboarding/components/configureStep/lineProtocol/LineProtocolTabs.tsx index 41d88d5f6d..36dadc565e 100644 --- a/ui/src/onboarding/components/configureStep/lineProtocol/LineProtocolTabs.tsx +++ b/ui/src/onboarding/components/configureStep/lineProtocol/LineProtocolTabs.tsx @@ -75,7 +75,7 @@ export class LineProtocolTabs extends PureComponent { {this.tabSelector}
{this.tabBody}
-
{this.submitButton}
+
{this.submitButton}
) } diff --git a/ui/src/onboarding/components/configureStep/lineProtocol/__snapshots__/LineProtocolTabs.test.tsx.snap b/ui/src/onboarding/components/configureStep/lineProtocol/__snapshots__/LineProtocolTabs.test.tsx.snap index d9ca14a81d..905718135b 100644 --- a/ui/src/onboarding/components/configureStep/lineProtocol/__snapshots__/LineProtocolTabs.test.tsx.snap +++ b/ui/src/onboarding/components/configureStep/lineProtocol/__snapshots__/LineProtocolTabs.test.tsx.snap @@ -49,7 +49,7 @@ exports[`LineProtocolTabs rendering renders! 1`] = ` />
`; diff --git a/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.test.tsx b/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.test.tsx index 2c5f14cbc8..b90f2442a0 100644 --- a/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.test.tsx +++ b/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.test.tsx @@ -12,7 +12,11 @@ import {ComponentStatus} from 'src/clockface' import {DataLoaderType} from 'src/types/v2/dataLoaders' // Dummy Data -import {defaultOnboardingStepProps, cpuTelegrafPlugin} from 'mocks/dummyData' +import { + defaultOnboardingStepProps, + telegrafPlugin, + cpuTelegrafPlugin, +} from 'mocks/dummyData' const setup = (override = {}) => { const props = { @@ -79,6 +83,29 @@ describe('Onboarding.Components.SelectionStep.SelectDataSourceStep', () => { }) }) + describe('skip link', () => { + it('does not render if telegraf no plugins are selected', () => { + const wrapper = setup() + const skipLink = wrapper.find('[data-test="skip"]') + + expect(skipLink.exists()).toBe(false) + }) + + it('renders if telegraf plugins are selected', () => { + const wrapper = setup({telegrafPlugins: [cpuTelegrafPlugin]}) + const skipLink = wrapper.find('[data-test="skip"]') + + expect(skipLink.exists()).toBe(true) + }) + + it('does not render if any telegraf plugins is incomplete', () => { + const wrapper = setup({telegrafPlugins: [telegrafPlugin]}) + const skipLink = wrapper.find('[data-test="skip"]') + + expect(skipLink.exists()).toBe(false) + }) + }) + describe('if type is line protocol', () => { it('renders back and next buttons with correct text', () => { const wrapper = setup({type: DataLoaderType.LineProtocol}) diff --git a/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.tsx b/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.tsx index a977f6b203..144a7a630c 100644 --- a/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.tsx +++ b/ui/src/onboarding/components/selectionStep/SelectDataSourceStep.tsx @@ -73,8 +73,8 @@ export class SelectDataSourceStep extends PureComponent { You will be able to configure additional Data Sources later {this.selector} -
-
+
+
+ const {telegrafPlugins} = this.props + + if (telegrafPlugins.length < 1) { + return + } + + const allConfigured = telegrafPlugins.every( + plugin => plugin.configured === 'configured' ) + + if (allConfigured) { + return ( +
) diff --git a/ui/src/dashboards/constants/index.ts b/ui/src/dashboards/constants/index.ts index 548f2b0a7b..b76b69082b 100644 --- a/ui/src/dashboards/constants/index.ts +++ b/ui/src/dashboards/constants/index.ts @@ -5,6 +5,7 @@ import { import {Cell} from 'src/types' import {DecimalPlaces} from 'src/types/v2/dashboards' import {Dashboard} from 'src/api' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' export const UNTITLED_GRAPH: string = 'Untitled Graph' @@ -34,13 +35,12 @@ export const DEFAULT_TABLE_OPTIONS = { fixFirstColumn: DEFAULT_FIX_FIRST_COLUMN, } -export const DEFAULT_TIME_FORMAT: string = 'MM/DD/YYYY HH:mm:ss' export const TIME_FORMAT_CUSTOM: string = 'Custom' export const FORMAT_OPTIONS: Array<{text: string}> = [ {text: DEFAULT_TIME_FORMAT}, {text: 'MM/DD/YYYY HH:mm:ss.SSS'}, - {text: 'YYYY-MM-DD HH:mm:ss'}, + {text: 'YYYY/MM/DD HH:mm:ss'}, {text: 'HH:mm:ss'}, {text: 'HH:mm:ss.SSS'}, {text: 'MMMM D, YYYY HH:mm:ss'}, diff --git a/ui/src/dashboards/utils/hoverTime.tsx b/ui/src/dashboards/utils/hoverTime.tsx index 48ad7c6328..3e91a6f833 100644 --- a/ui/src/dashboards/utils/hoverTime.tsx +++ b/ui/src/dashboards/utils/hoverTime.tsx @@ -2,9 +2,7 @@ import React, {PureComponent} from 'react' export interface InjectedHoverProps { hoverTime: number | null - activeViewID: string | null onSetHoverTime: (hoverTime: number | null) => void - onSetActiveViewID: (activeViewID: string) => void } const {Provider, Consumer} = React.createContext(null) @@ -12,10 +10,7 @@ const {Provider, Consumer} = React.createContext(null) export class HoverTimeProvider extends PureComponent<{}, InjectedHoverProps> { public state: InjectedHoverProps = { hoverTime: null, - activeViewID: null, onSetHoverTime: (hoverTime: number | null) => this.setState({hoverTime}), - onSetActiveViewID: (activeViewID: string | null) => - this.setState({activeViewID}), } public render() { diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 7591affd05..01f654ecbd 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -3,11 +3,8 @@ import _ from 'lodash' import {fastMap, fastReduce, fastFilter} from 'src/utils/fast' import {CELL_HORIZONTAL_PADDING} from 'src/shared/constants/tableGraph' -import { - DEFAULT_TIME_FIELD, - DEFAULT_TIME_FORMAT, - TimeField, -} from 'src/dashboards/constants' +import {DEFAULT_TIME_FIELD, TimeField} from 'src/dashboards/constants' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' import { Sort, FieldOption, diff --git a/ui/src/logs/components/logs_table/LogsTable.tsx b/ui/src/logs/components/logs_table/LogsTable.tsx index 011e079c0b..3453f1ea31 100644 --- a/ui/src/logs/components/logs_table/LogsTable.tsx +++ b/ui/src/logs/components/logs_table/LogsTable.tsx @@ -31,7 +31,7 @@ import { } from 'src/logs/utils/table' // Constants -import {DEFAULT_TIME_FORMAT} from 'src/logs/constants' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' import {INITIAL_LIMIT} from 'src/logs/actions' // Types diff --git a/ui/src/logs/constants/index.ts b/ui/src/logs/constants/index.ts index cb8478b5bf..1935d43096 100644 --- a/ui/src/logs/constants/index.ts +++ b/ui/src/logs/constants/index.ts @@ -8,7 +8,6 @@ import { export const NOW = 0 export const DEFAULT_TRUNCATION = true -export const DEFAULT_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' export const LOG_VIEW_NAME = 'LOGS_PAGE' diff --git a/ui/src/logs/utils/table.ts b/ui/src/logs/utils/table.ts index c1013a6dcb..6172f51bd7 100644 --- a/ui/src/logs/utils/table.ts +++ b/ui/src/logs/utils/table.ts @@ -7,7 +7,7 @@ import { SeverityFormat, SeverityFormatOptions, } from 'src/types/logs' -import {DEFAULT_TIME_FORMAT} from 'src/logs/constants' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' import { orderTableColumns, filterTableColumns, diff --git a/ui/src/logs/utils/v1/queryBuilder.ts b/ui/src/logs/utils/v1/queryBuilder.ts index 55bc2d7488..468fae9428 100644 --- a/ui/src/logs/utils/v1/queryBuilder.ts +++ b/ui/src/logs/utils/v1/queryBuilder.ts @@ -12,7 +12,7 @@ import { buildFill, } from 'src/utils/influxql' -import {DEFAULT_TIME_FORMAT} from 'src/logs/constants' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' const keyMapping = (key: string): string => { switch (key) { diff --git a/ui/src/logs/utils/v2/index.ts b/ui/src/logs/utils/v2/index.ts index 042ca8032f..eb59d59835 100644 --- a/ui/src/logs/utils/v2/index.ts +++ b/ui/src/logs/utils/v2/index.ts @@ -1,6 +1,6 @@ import moment from 'moment' -import {DEFAULT_TIME_FORMAT} from 'src/logs/constants' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' import {getDeep} from 'src/utils/wrappers' diff --git a/ui/src/shared/components/DygraphContainer.tsx b/ui/src/shared/components/DygraphContainer.tsx index b8699394be..d426ef408b 100644 --- a/ui/src/shared/components/DygraphContainer.tsx +++ b/ui/src/shared/components/DygraphContainer.tsx @@ -38,13 +38,14 @@ const DygraphContainer: SFC = props => { return ( - {({labels, dygraphsData}) => ( + {({labels, dygraphsData, seriesDescriptions}) => ( void - onShow: (e: MouseEvent) => void - onMouseEnter: () => void -} - -interface LegendData { - x: number - series: SeriesLegendData[] - xHTML: string -} - -interface State { - legend: LegendData - sortType: string - isAscending: boolean - filterText: string - isFilterVisible: boolean - legendStyles: object - pageX: number | null - viewID: string -} - -type Props = OwnProps & InjectedHoverProps - -@ErrorHandling -class DygraphLegend extends PureComponent { - private legendRef: HTMLElement | null = null - - constructor(props: Props) { - super(props) - - this.props.dygraph.updateOptions({ - legendFormatter: this.legendFormatter, - highlightCallback: this.highlightCallback, - unhighlightCallback: this.unhighlightCallback, - }) - - this.state = { - legend: { - x: null, - series: [], - xHTML: '', - }, - sortType: 'numeric', - isAscending: false, - filterText: '', - isFilterVisible: false, - legendStyles: {}, - pageX: null, - viewID: null, - } - } - - public componentWillUnmount() { - if ( - !this.props.dygraph.graphDiv || - !this.props.dygraph.visibility().find(bool => bool === true) - ) { - this.setState({filterText: ''}) - } - } - - public render() { - const {onMouseEnter} = this.props - const {legend, filterText, isAscending, isFilterVisible} = this.state - - return ( -
(this.legendRef = el)} - onMouseEnter={onMouseEnter} - onMouseLeave={this.handleHide} - style={this.styles} - > -
-
{legend.xHTML}
- - - -
- {isFilterVisible && ( - - )} -
- {this.filtered.map(({label, color, yHTML, isHighlighted}) => { - const seriesClass = isHighlighted - ? 'dygraph-legend--row highlight' - : 'dygraph-legend--row' - return ( -
- {label} -
{yHTML || 'no value'}
-
- ) - })} -
-
- ) - } - - private handleHide = (): void => { - this.props.onHide() - this.props.onSetActiveViewID(null) - } - - private handleToggleFilter = (): void => { - this.setState({ - isFilterVisible: !this.state.isFilterVisible, - filterText: '', - }) - } - - private handleLegendInputChange = ( - e: ChangeEvent - ): void => { - const {dygraph} = this.props - const {legend} = this.state - const filterText = e.target.value - - legend.series.map((__, i) => { - if (!legend.series[i]) { - return dygraph.setVisibility(i, true) - } - - dygraph.setVisibility(i, !!legend.series[i].label.match(filterText)) - }) - - this.setState({filterText}) - } - - private handleSortLegend = (sortType: string) => () => { - this.setState({sortType, isAscending: !this.state.isAscending}) - } - - private highlightCallback = (e: MouseEvent) => { - if (this.props.activeViewID !== this.props.viewID) { - this.props.onSetActiveViewID(this.props.viewID) - } - - this.setState({pageX: e.pageX}) - this.props.onShow(e) - } - - private legendFormatter = (legend: LegendData) => { - if (!legend.x) { - return '' - } - - const {legend: prevLegend} = this.state - const highlighted = legend.series.find(s => s.isHighlighted) - const prevHighlighted = prevLegend.series.find(s => s.isHighlighted) - - const yVal = highlighted && highlighted.y - const prevY = prevHighlighted && prevHighlighted.y - - if (legend.x === prevLegend.x && yVal === prevY) { - return '' - } - - this.setState({legend}) - return '' - } - - private unhighlightCallback = (e: MouseEvent) => { - const {top, bottom, left, right} = this.legendRef.getBoundingClientRect() - - const mouseY = e.clientY - const mouseX = e.clientX - - const mouseBuffer = 5 - const mouseInLegendY = mouseY <= bottom && mouseY >= top - mouseBuffer - const mouseInLegendX = mouseX <= right && mouseX >= left - const isMouseHoveringLegend = mouseInLegendY && mouseInLegendX - - if (!isMouseHoveringLegend) { - this.handleHide() - } - } - - private get filtered(): SeriesLegendData[] { - const {legend, sortType, isAscending, filterText} = this.state - const withValues = legend.series.filter(s => !_.isNil(s.y)) - const sorted = _.sortBy( - withValues, - ({y, label}) => (sortType === 'numeric' ? y : label) - ) - - const ordered = isAscending ? sorted : sorted.reverse() - return ordered.filter(s => s.label.match(filterText)) - } - - private get isAlphaSort(): boolean { - return this.state.sortType === 'alphabetic' - } - - private get isNumSort(): boolean { - return this.state.sortType === 'numeric' - } - - private get isVisible(): boolean { - const {viewID, activeViewID} = this.props - - return viewID === activeViewID - } - - private get hidden(): string { - if (this.isVisible) { - return '' - } - - return 'hidden' - } - - private get styles() { - const { - dygraph, - dygraph: {graphDiv}, - hoverTime, - } = this.props - - const cursorOffset = 16 - const legendPosition = dygraph.toDomXCoord(hoverTime) + cursorOffset - return makeLegendStyles(graphDiv, this.legendRef, legendPosition) - } -} - -export default withHoverTime(DygraphLegend) diff --git a/ui/src/shared/components/DygraphLegendSort.tsx b/ui/src/shared/components/DygraphLegendSort.tsx deleted file mode 100644 index 5b5139241b..0000000000 --- a/ui/src/shared/components/DygraphLegendSort.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {PureComponent} from 'react' -import classnames from 'classnames' - -interface Props { - isActive: boolean - isAscending: boolean - top: string - bottom: string - onSort: () => void -} - -class DygraphLegendSort extends PureComponent { - public render() { - const {isAscending, top, bottom, onSort, isActive} = this.props - return ( -
-
-
{top}
-
{bottom}
-
-
- ) - } -} - -export default DygraphLegendSort diff --git a/ui/src/shared/components/DygraphTransformation.tsx b/ui/src/shared/components/DygraphTransformation.tsx index 7b07db18fc..1063a6b86c 100644 --- a/ui/src/shared/components/DygraphTransformation.tsx +++ b/ui/src/shared/components/DygraphTransformation.tsx @@ -27,6 +27,7 @@ class DygraphTransformation extends PureComponent< this.state = { labels: [], dygraphsData: [], + seriesDescriptions: [], } } diff --git a/ui/src/shared/components/HoverTimeMarker.scss b/ui/src/shared/components/HoverTimeMarker.scss new file mode 100644 index 0000000000..1b626f7391 --- /dev/null +++ b/ui/src/shared/components/HoverTimeMarker.scss @@ -0,0 +1,8 @@ +@import "src/style/modules"; + +.hover-time-marker { + position: absolute; + height: calc(100% - 40px); + top: 10px; + border-left: 1px solid $g8-storm; +} diff --git a/ui/src/shared/components/HoverTimeMarker.tsx b/ui/src/shared/components/HoverTimeMarker.tsx new file mode 100644 index 0000000000..688fb209e5 --- /dev/null +++ b/ui/src/shared/components/HoverTimeMarker.tsx @@ -0,0 +1,17 @@ +import React, {SFC} from 'react' + +import 'src/shared/components/HoverTimeMarker.scss' + +const MARGIN_LEFT = 10 + +interface Props { + x: number +} + +const HoverTimeMarker: SFC = props => { + const style = {left: `${props.x + MARGIN_LEFT}px`} + + return
+} + +export default HoverTimeMarker diff --git a/ui/src/shared/components/Legend.scss b/ui/src/shared/components/Legend.scss new file mode 100644 index 0000000000..736b2919c1 --- /dev/null +++ b/ui/src/shared/components/Legend.scss @@ -0,0 +1,59 @@ +@import "src/style/modules"; + +.legend { + position: fixed; + top: 0; + max-height: 160px; + max-width: 800px; + background: $g2-kevlar; + z-index: $z--dygraph-legend; + border: 1px solid $g3-castle; + padding: 10px; + border-radius: $ix-radius; + color: $g14-chromium; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; + font-family: $code-font; + letter-spacing: -0.5px; +} + +.legend--time { + margin-bottom: 10px; +} + +.legend--columns { + display: flex; + justify-content: flex-start; + align-items: flex-start; +} + +.legend--column { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + margin-right: 15px; + + &:last-child { + margin-right: 0; + } +} + +.legend--column.numeric { + align-items: flex-end; +} + +.legend--column-header { + color: $g10-wolf; + margin-bottom: 5px; +} + +.legend--column-row { + margin-bottom: 2px; + + &.empty:after { + content: '—'; + color: $g8-storm; + } +} diff --git a/ui/src/shared/components/Legend.tsx b/ui/src/shared/components/Legend.tsx new file mode 100644 index 0000000000..2623e53497 --- /dev/null +++ b/ui/src/shared/components/Legend.tsx @@ -0,0 +1,186 @@ +// Libraries +import React, {PureComponent, CSSProperties} from 'react' +import {createPortal} from 'react-dom' +import moment from 'moment' +import {uniq, flatten, isNumber} from 'lodash' + +// Components +import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' + +// Constants +import {LEGEND_PORTAL_ID} from 'src/shared/components/LegendPortal' +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' + +// Styles +import 'src/shared/components/Legend.scss' + +// Types +import {SeriesDescription} from 'src/shared/parsing/flux/spreadTables' + +interface Props { + seriesDescriptions: SeriesDescription[] + time: number + values: {[seriesKey: string]: number} + x: number + colors: {[seriesKey: string]: string} + visRect: DOMRect +} + +const VALUE_PRECISION = 2 +const NOISY_COLUMNS = new Set(['_start', '_stop', 'result']) +const LEGEND_MARGIN = 10 + +class Legend extends PureComponent { + private legendRef = React.createRef() + + public componentDidMount() { + this.setPosition() + } + + public componentDidUpdate() { + this.setPosition() + } + + public render() { + return createPortal( +
+
{this.time}
+ +
+ {this.columns.map(({name, isNumeric, rows}) => ( +
+
{name}
+ {rows.map(({color, value}) => ( +
+ {isNumber(value) ? value.toFixed(VALUE_PRECISION) : value} +
+ ))} +
+ ))} +
+
+
, + document.querySelector(`#${LEGEND_PORTAL_ID}`) + ) + } + + private get time() { + return moment(this.props.time).format(DEFAULT_TIME_FORMAT) + } + + private get columns() { + const {seriesDescriptions, values, colors} = this.props + + const metaColumnNames = uniq( + flatten(seriesDescriptions.map(d => Object.keys(d.metaColumns))) + ) + + let columns = metaColumnNames.map(name => ({ + name, + isNumeric: false, + rows: [], + })) + + const descsByKey = seriesDescriptions.reduce( + (acc, d) => ({...acc, [d.key]: d}), + {} + ) + + const seriesKeys = Object.keys(values) + + // Sort series so that those with higher values are first + seriesKeys.sort((a, b) => values[b] - values[a]) + + for (const column of columns) { + for (const key of seriesKeys) { + column.rows.push({ + color: colors[key], + value: descsByKey[key].metaColumns[column.name], + }) + } + } + + // Don't show noisy columns like `_start` and `_stop` if the value for every + // series in that column will be the same + columns = columns.filter( + col => + !NOISY_COLUMNS.has(col.name) || + !col.rows.every(d => d.value === col.rows[0].value) + ) + + const valueColumnNames = uniq( + seriesDescriptions.map(d => d.valueColumnName) + ) + + for (const name of valueColumnNames) { + const rows = seriesKeys.map(key => { + let value + + if (descsByKey[key].valueColumnName === name) { + value = values[key] + } + + return {value, color: colors[key]} + }) + + columns.push({name, rows, isNumeric: true}) + } + + return columns + } + + private get style(): CSSProperties { + const {x, visRect} = this.props + const legendRect = this.legendRef.current.getBoundingClientRect() + + let left = x + visRect.left - legendRect.width / 2 + + if (left + legendRect.width > window.innerWidth - LEGEND_MARGIN) { + left = window.innerWidth - legendRect.width - LEGEND_MARGIN + } else if (left < LEGEND_MARGIN) { + left = LEGEND_MARGIN + } + + let top = visRect.top - legendRect.height + + if (top < LEGEND_MARGIN) { + top = visRect.bottom + } + + if (top + legendRect.height > window.innerHeight - LEGEND_MARGIN) { + top = LEGEND_MARGIN + } + + const style: CSSProperties = { + left: `${left}px`, + top: `${top}px`, + } + + return style + } + + private setPosition(): void { + if (!this.legendRef.current) { + return + } + + // We update the placement of the legend by mutating it's `style` attribute + // after it has rendered, so that we can use the size of the legend to + // calculate a reasonable placement + const styleAttr = Object.entries(this.style).reduce( + (acc, [k, v]) => `${acc}; ${k}: ${v}`, + '' + ) + + this.legendRef.current.setAttribute('style', styleAttr) + } +} + +export default Legend diff --git a/ui/src/shared/components/LegendPortal.tsx b/ui/src/shared/components/LegendPortal.tsx new file mode 100644 index 0000000000..837816a882 --- /dev/null +++ b/ui/src/shared/components/LegendPortal.tsx @@ -0,0 +1,9 @@ +import React, {SFC} from 'react' + +export const LEGEND_PORTAL_ID = 'legend-portal' + +const LegendPortal: SFC = () => { + return
+} + +export default LegendPortal diff --git a/ui/src/shared/components/crosshair/Crosshair.scss b/ui/src/shared/components/crosshair/Crosshair.scss deleted file mode 100644 index b2808398c0..0000000000 --- a/ui/src/shared/components/crosshair/Crosshair.scss +++ /dev/null @@ -1,15 +0,0 @@ -/* - Crosshairs - ------------------------------------------------------------------------------ -*/ - -.crosshair { - left: 0; - top: 0; - position: absolute; - width: 1px; - z-index: 3; - background: linear-gradient(to bottom, fade-out($g14-chromium, 1) 0%,$g14-chromium 7%,$g14-chromium 93%,fade-out($g14-chromium, 1) 100%); - pointer-events: none; - min-height: 20px; -} diff --git a/ui/src/shared/components/crosshair/Crosshair.tsx b/ui/src/shared/components/crosshair/Crosshair.tsx deleted file mode 100644 index 806fa68cac..0000000000 --- a/ui/src/shared/components/crosshair/Crosshair.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import _ from 'lodash' -import React, {PureComponent} from 'react' -import Dygraph from 'dygraphs' - -import {ErrorHandling} from 'src/shared/decorators/errors' - -import {withHoverTime, InjectedHoverProps} from 'src/dashboards/utils/hoverTime' - -import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'src/shared/constants' - -interface OwnProps { - dygraph: Dygraph -} - -type Props = OwnProps & InjectedHoverProps - -@ErrorHandling -class Crosshair extends PureComponent { - public render() { - if (!this.isVisible) { - return
- } - - return ( -
-
-
- ) - } - - private get isVisible() { - const {dygraph, hoverTime} = this.props - const timeRanges = dygraph.xAxisRange() - - const minTimeRange = _.get(timeRanges, '0', 0) - const isBeforeMinTimeRange = hoverTime < minTimeRange - - const maxTimeRange = _.get(timeRanges, '1', Infinity) - const isPastMaxTimeRange = hoverTime > maxTimeRange - - const isValidHoverTime = !isBeforeMinTimeRange && !isPastMaxTimeRange - return isValidHoverTime && hoverTime !== 0 && _.isFinite(hoverTime) - } - - private get crosshairLeft(): string { - const {dygraph, hoverTime} = this.props - const cursorOffset = 16 - return `translateX(${dygraph.toDomXCoord(hoverTime) + cursorOffset}px)` - } - - private get crosshairHeight(): string { - return `calc(100% - ${DYGRAPH_CONTAINER_XLABEL_MARGIN}px)` - } -} - -export default withHoverTime(Crosshair) diff --git a/ui/src/shared/components/dygraph/Dygraph.scss b/ui/src/shared/components/dygraph/Dygraph.scss index f1bc1231f6..d8e393194e 100644 --- a/ui/src/shared/components/dygraph/Dygraph.scss +++ b/ui/src/shared/components/dygraph/Dygraph.scss @@ -22,6 +22,7 @@ $dygraph--padding: 10px; right: $dygraph--padding; bottom: $dygraph--padding; left: $dygraph--padding; + overflow: hidden; } .dygraph-child-container { @@ -81,160 +82,3 @@ $dygraph--padding: 10px; padding: 0 0 0 4px !important; } } - -/* - Legend Styles - ------------------------------------------------------------------------------ -*/ -.dygraph-child-container .dygraph-legend { - display: none !important; // hide default legend -} -.dygraph-legend { - background-color: $g0-obsidian; - display: block !important; - position: absolute; - padding: 8px; - z-index: $z--dygraph-legend; - border-radius: 3px; - user-select: text; - transform: translateX(-50%); - @extend %drop-shadow; - - &.hidden { - display: none !important; - } - - // Arrow (default is on top of legend aka below graph) - &:after { - content: ''; - position: absolute; - border-width: 8px; - border-style: solid; - border-color: transparent; - } - &.dygraph-legend--top:after { - top: -16px; - border-bottom-color: $g0-obsidian; - left: 50%; - transform: translateX(-50%); - } - &.dygraph-legend--bottom:after { - bottom: -16px; - border-top-color: $g0-obsidian; - left: 50%; - transform: translateX(-50%); - } - &.dygraph-legend--left:after { - left: -16px; - border-right-color: $g0-obsidian; - top: 50%; - transform: translateY(-50%); - } - &.dygraph-legend--right:after { - right: -16px; - border-left-color: $g0-obsidian; - top: 50%; - transform: translateY(-50%); - } -} -.dygraph-legend--header { - display: flex; - align-items: center; - flex-wrap: nowrap; - - > .btn { - margin-left: 4px; - } -} -.dygraph-legend--timestamp { - margin-right: 8px; - font-size: 12px; - white-space: nowrap; - font-weight: 600; - color: $g13-mist; - flex: 1 0 0; -} -.dygraph-legend--filter { - flex: 1 0 0; - margin-top: 8px; -} -.dygraph-legend--contents { - font-size: 13px; - color: $g15-platinum; - font-weight: 600; - line-height: 13px; - max-height: 123px; - margin-top: 8px; - overflow-y: auto; - @include custom-scrollbar-round($g0-obsidian, $g3-castle); -} -.dygraph-legend--row { - display: flex; - align-items: flex-start; - justify-content: space-between; - flex-wrap: nowrap; - opacity: 1; - font-size: 11px; - line-height: 11px; - font-weight: 600; - padding: 3px 0; - - span { - font-weight: 900; - padding: 0; - white-space: nowrap; - } - figure { - white-space: nowrap; - padding-left: 10px; - font-family: $code-font; - } - - &.highlight { - opacity: 1; - background-color: $g3-castle; - figure { - color: $g20-white; - } - } - &.highlight:only-child { - background-color: transparent; - } -} - -// Sorting Buttons -.sort-btn { - position: relative; -} -.sort-btn--rotator { - width: 100%; - height: 100%; - position: absolute; - transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} -.sort-btn--top, -.sort-btn--bottom { - position: absolute; - font-size: 10px; - font-weight: 900; - color: $g20-white; - transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} -.sort-btn--top { - left: 3px; - top: -3px; -} -.sort-btn--bottom { - bottom: -4px; - left: 12px; -} -// Toggled State -.sort-btn.sort-btn--desc { - .sort-btn--rotator { - transform: rotate(180deg); - } - .sort-btn--top, - .sort-btn--bottom { - transform: rotate(-180deg); - } -} diff --git a/ui/src/shared/components/dygraph/Dygraph.tsx b/ui/src/shared/components/dygraph/Dygraph.tsx index f1ee58663a..f440dc00f6 100644 --- a/ui/src/shared/components/dygraph/Dygraph.tsx +++ b/ui/src/shared/components/dygraph/Dygraph.tsx @@ -1,5 +1,5 @@ // Libraries -import React, {Component, MouseEvent} from 'react' +import React, {Component} from 'react' import {get, filter, isEqual} from 'lodash' import NanoDate from 'nano-date' import ReactResizeDetector from 'react-resize-detector' @@ -7,9 +7,9 @@ import memoizeOne from 'memoize-one' // Components import Dygraphs from 'src/external/dygraph' -import DygraphLegend from 'src/shared/components/DygraphLegend' -import Crosshair from 'src/shared/components/crosshair/Crosshair' +import Legend from 'src/shared/components/Legend' import {ErrorHandling} from 'src/shared/decorators/errors' +import HoverTimeMarker from 'src/shared/components/HoverTimeMarker' // Utils import getRange, {getStackedRange} from 'src/shared/parsing/getRangeForDygraph' @@ -26,9 +26,10 @@ import { // Types import {Axes, TimeRange} from 'src/types' -import {DygraphData, Options} from 'src/external/dygraph' +import {DygraphData, Options, SeriesLegendData} from 'src/external/dygraph' import {Color} from 'src/types/colors' import {DashboardQuery} from 'src/types/v2/dashboards' +import {SeriesDescription} from 'src/shared/parsing/flux/spreadTables' const getRangeMemoizedY = memoizeOne(getRange) @@ -46,11 +47,19 @@ const DEFAULT_DYGRAPH_OPTIONS = { connectSeparatedPoints: true, } +interface LegendData { + x: number + series: SeriesLegendData[] + xHTML: string + dygraph: Dygraphs +} + interface OwnProps { viewID: string queries?: DashboardQuery[] timeSeries: DygraphData labels: string[] + seriesDescriptions: SeriesDescription[] options?: Partial colors: Color[] timeRange?: TimeRange @@ -64,8 +73,13 @@ interface OwnProps { type Props = OwnProps & InjectedHoverProps interface State { - xAxisRange: [number, number] - isMouseInLegend: boolean + legendData?: { + time: number + x: number + visRect: DOMRect + values: {[seriesKey: string]: number} + colors: {[seriesKey: string]: string} + } } @ErrorHandling @@ -91,24 +105,19 @@ class Dygraph extends Component { options: {}, } - private graphRef: React.RefObject + public state: State = {} + + private graphRef: React.RefObject = React.createRef() private dygraph: Dygraphs private dygraphOptions?: Options - constructor(props: Props) { - super(props) - - this.state = { - xAxisRange: [0, 0], - isMouseInLegend: false, - } - - this.graphRef = React.createRef() - } - public componentDidMount() { const options = this.collectDygraphOptions() - const initialOptions = {...DEFAULT_DYGRAPH_OPTIONS, ...options} + const initialOptions = { + ...DEFAULT_DYGRAPH_OPTIONS, + ...options, + legendFormatter: this.captureLegendData, + } this.dygraph = new Dygraphs( this.graphRef.current, @@ -117,7 +126,6 @@ class Dygraph extends Component { ) this.dygraphOptions = options - this.setState({xAxisRange: this.dygraph.xAxisRange()}) } public componentWillUnmount() { @@ -148,25 +156,16 @@ class Dygraph extends Component { } public render() { - const {viewID} = this.props + const {viewID, seriesDescriptions, hoverTime} = this.props + const {legendData} = this.state return ( -
- {this.dygraph && ( -
- - -
+
+ {legendData && ( + + )} + {hoverTime && ( + )} {this.nestedGraph}
{ return onZoom({lower: null, upper: null}) } - private handleDraw = () => { - if (!this.dygraph) { - return - } - - const {xAxisRange} = this.state - const newXAxisRange = this.dygraph.xAxisRange() - - if (!isEqual(xAxisRange, newXAxisRange)) { - this.setState({xAxisRange: newXAxisRange}) - } - } - private formatYVal = ( yval: number, __, @@ -291,35 +277,6 @@ class Dygraph extends Component { return numberValueFormatter(yval, opts, prefix, suffix) } - private eventToTimestamp = ({ - pageX: pxBetweenMouseAndPage, - }: MouseEvent): number => { - const pxBetweenGraphAndPage = this.graphRef.current.getBoundingClientRect() - .left - const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage - const timestamp = this.dygraph.toDataXCoord(graphXCoordinate) - const [xRangeStart] = this.dygraph.xAxisRange() - const clamped = Math.max(xRangeStart, timestamp) - - return clamped - } - - private handleHideLegend = () => { - this.setState({isMouseInLegend: false}) - this.props.onSetHoverTime(null) - } - - private handleShowLegend = (e: MouseEvent): void => { - const {isMouseInLegend} = this.state - - if (isMouseInLegend) { - return - } - - const newTime = this.eventToTimestamp(e) - this.props.onSetHoverTime(newTime) - } - private collectDygraphOptions(): Options { const { labels, @@ -330,7 +287,6 @@ class Dygraph extends Component { } = this.props const { - handleDraw, handleZoom, timeSeries, labelWidth, @@ -345,7 +301,6 @@ class Dygraph extends Component { colors, file: timeSeries as any, zoomCallback: handleZoom, - drawCallback: handleDraw, fillGraph: isGraphFilled, logscale: y.scale === LOG, ylabel: yLabel, @@ -402,8 +357,45 @@ class Dygraph extends Component { return nanoDate.toISOString() } - private handleMouseEnterLegend = () => { - this.setState({isMouseInLegend: true}) + private handleMouseLeave = () => { + this.setState({legendData: null}) + this.props.onSetHoverTime(null) + } + + private captureLegendData = ({x: time, dygraph, series}: LegendData) => { + if (!time) { + return '' + } + + const values = series.reduce( + (acc, d) => ({ + ...acc, + [d.label]: d.y, + }), + {} + ) + + const colors = series.reduce( + (acc, d) => ({ + ...acc, + [d.label]: d.color, + }), + {} + ) + + this.setState({ + legendData: { + time, + values, + colors, + x: dygraph.toDomXCoord(time), + visRect: this.graphRef.current.getBoundingClientRect() as DOMRect, + }, + }) + + this.props.onSetHoverTime(time) + + return '' } } diff --git a/ui/src/shared/components/view_options/options/TimeFormat.tsx b/ui/src/shared/components/view_options/options/TimeFormat.tsx index f71bcff7ea..7547a2217f 100644 --- a/ui/src/shared/components/view_options/options/TimeFormat.tsx +++ b/ui/src/shared/components/view_options/options/TimeFormat.tsx @@ -5,10 +5,10 @@ import {Form, Input, InputType, Dropdown, Columns} from 'src/clockface' import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip' // Constants +import {DEFAULT_TIME_FORMAT} from 'src/shared/constants' import { FORMAT_OPTIONS, TIME_FORMAT_CUSTOM, - DEFAULT_TIME_FORMAT, TIME_FORMAT_TOOLTIP_LINK, } from 'src/dashboards/constants' diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 11c7fae5d0..38cb0b3434 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -2,6 +2,8 @@ import _ from 'lodash' import {TemplateValueType, TemplateType} from 'src/types' +export const DEFAULT_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' + export const DEFAULT_DURATION_MS = 1000 export const DEFAULT_PIXELS = 333 diff --git a/ui/src/shared/parsing/flux/dygraph.test.ts b/ui/src/shared/parsing/flux/dygraph.test.ts index 1b3bc7b6ce..df669bd515 100644 --- a/ui/src/shared/parsing/flux/dygraph.test.ts +++ b/ui/src/shared/parsing/flux/dygraph.test.ts @@ -35,40 +35,36 @@ describe('fluxTablesToDygraph', () => { it('can parse multiple values per row', () => { const fluxTables = parseResponse(MULTI_VALUE_ROW) const actual = fluxTablesToDygraph(fluxTables) - const expected = { - labels: [ - 'time', - 'mean_usage_idle[result=0][_measurement=cpu]', - 'mean_usage_idle[result=0][_measurement=mem]', - 'mean_usage_user[result=0][_measurement=cpu]', - 'mean_usage_user[result=0][_measurement=mem]', - ], - dygraphsData: [ - [new Date('2018-09-10T16:54:37Z'), 85, 8, 10, 1], - [new Date('2018-09-10T16:54:38Z'), 87, 9, 7, 2], - [new Date('2018-09-10T16:54:39Z'), 89, 10, 5, 3], - ], - } - expect(actual).toEqual(expected) + expect(actual.dygraphsData).toEqual([ + [new Date('2018-09-10T16:54:37Z'), 85, 8, 10, 1], + [new Date('2018-09-10T16:54:38Z'), 87, 9, 7, 2], + [new Date('2018-09-10T16:54:39Z'), 89, 10, 5, 3], + ]) + + expect(actual.labels).toEqual([ + 'time', + 'mean_usage_idle[result=0][_measurement=cpu]', + 'mean_usage_idle[result=0][_measurement=mem]', + 'mean_usage_user[result=0][_measurement=cpu]', + 'mean_usage_user[result=0][_measurement=mem]', + ]) }) it('filters out non-numeric series', () => { const fluxTables = parseResponse(MIXED_DATATYPES) const actual = fluxTablesToDygraph(fluxTables) - const expected = { - labels: [ - 'time', - 'mean_usage_idle[result=0][_measurement=cpu]', - 'mean_usage_idle[result=0][_measurement=mem]', - ], - dygraphsData: [ - [new Date('2018-09-10T16:54:37Z'), 85, 8], - [new Date('2018-09-10T16:54:39Z'), 89, 10], - ], - } - expect(actual).toEqual(expected) + expect(actual.dygraphsData).toEqual([ + [new Date('2018-09-10T16:54:37Z'), 85, 8], + [new Date('2018-09-10T16:54:39Z'), 89, 10], + ]) + + expect(actual.labels).toEqual([ + 'time', + 'mean_usage_idle[result=0][_measurement=cpu]', + 'mean_usage_idle[result=0][_measurement=mem]', + ]) }) it('can parse identical series in different results', () => { @@ -90,20 +86,18 @@ describe('fluxTablesToDygraph', () => { ` const fluxTables = parseResponse(resp) const actual = fluxTablesToDygraph(fluxTables) - const expected = { - dygraphsData: [ - [new Date('2018-12-10T18:29:48.000Z'), undefined, 4589981696], - [new Date('2018-12-10T18:29:58.000Z'), 4906213376, undefined], - [new Date('2018-12-10T18:40:18.000Z'), undefined, 4318040064], - [new Date('2018-12-10T18:54:08.000Z'), 5860683776, undefined], - ], - labels: [ - 'time', - '_value[result=0][_field=active][_measurement=mem][host=oox4k.local]', - '_value[result=1][_field=active][_measurement=mem][host=oox4k.local]', - ], - } - expect(actual).toEqual(expected) + expect(actual.dygraphsData).toEqual([ + [new Date('2018-12-10T18:29:48.000Z'), undefined, 4589981696], + [new Date('2018-12-10T18:29:58.000Z'), 4906213376, undefined], + [new Date('2018-12-10T18:40:18.000Z'), undefined, 4318040064], + [new Date('2018-12-10T18:54:08.000Z'), 5860683776, undefined], + ]) + + expect(actual.labels).toEqual([ + 'time', + '_value[result=0][_field=active][_measurement=mem][host=oox4k.local]', + '_value[result=1][_field=active][_measurement=mem][host=oox4k.local]', + ]) }) }) diff --git a/ui/src/shared/parsing/flux/dygraph.ts b/ui/src/shared/parsing/flux/dygraph.ts index b2454a5c0d..e73c8f9e01 100644 --- a/ui/src/shared/parsing/flux/dygraph.ts +++ b/ui/src/shared/parsing/flux/dygraph.ts @@ -2,7 +2,10 @@ import _ from 'lodash' // Utils -import {spreadTables} from 'src/shared/parsing/flux/spreadTables' +import { + spreadTables, + SeriesDescription, +} from 'src/shared/parsing/flux/spreadTables' // Types import {FluxTable} from 'src/types' @@ -11,6 +14,7 @@ import {DygraphValue} from 'src/external/dygraph' export interface FluxTablesToDygraphResult { labels: string[] dygraphsData: DygraphValue[][] + seriesDescriptions: SeriesDescription[] } export const fluxTablesToDygraph = ( @@ -28,5 +32,5 @@ export const fluxTablesToDygraph = ( dygraphsData.sort((a, b) => (a[0] as any) - (b[0] as any)) - return {dygraphsData, labels: ['time', ...labels]} + return {dygraphsData, labels: ['time', ...labels], seriesDescriptions} } diff --git a/ui/src/shared/parsing/flux/spreadTables.ts b/ui/src/shared/parsing/flux/spreadTables.ts index cd933fc88b..f955a51ad1 100644 --- a/ui/src/shared/parsing/flux/spreadTables.ts +++ b/ui/src/shared/parsing/flux/spreadTables.ts @@ -1,6 +1,6 @@ import {FluxTable} from 'src/types' -interface SeriesDescription { +export interface SeriesDescription { // A key identifying a unique (column, table, result) triple for a particular // Flux response—i.e. a single time series key: string diff --git a/ui/src/style/_variables.scss b/ui/src/style/_variables.scss index f0a615be34..c27c4d920a 100644 --- a/ui/src/style/_variables.scss +++ b/ui/src/style/_variables.scss @@ -20,7 +20,7 @@ $z--notifications: 9999; $z--right-click-layer: 9995; $z--overlays: 9990; $z--drag-n-drop: 5000; -$z--dygraph-legend: 4000; +$z--dygraph-legend: 9993; $z--cell-default: 1; $z--cell-dragging: 3; diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 0b699c4f17..356357caf7 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -23,7 +23,6 @@ @import "src/shared/components/fancy_scrollbar/FancyScrollbar"; @import "src/shared/components/notifications/Notifications"; @import "src/shared/components/threesizer/Threesizer"; -@import "src/shared/components/crosshair/Crosshair"; @import "src/shared/components/graph_tips/GraphTips"; @import "src/shared/components/page_spinner/PageSpinner"; @import "src/shared/components/cells/Dashboards";