diff --git a/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap new file mode 100644 index 0000000000..1efafd0e03 --- /dev/null +++ b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap @@ -0,0 +1,2022 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Integration Setup Page Renders integration setup page as expected 1`] = ` + + +
+ +
+ +
+ +
+ , + "status": "", + "title": "Name Integration", + }, + Object { + "children": , + "status": "disabled", + "title": "Select index or data source for integration", + }, + ] + } + > +
+ +
+
+ + + + + Step 1 + + + + + + +

+ Name Integration +

+
+
+
+ +
+ +
+
+ + +
+
+ + + + + Step 2 is disabled + + + + + + +

+ Select index or data source for integration +

+
+
+
+ +
+ +
+
+ +
+ +
+ + +
+ + + +
+ +

+ Name Integration +

+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ The name will be used to label the newly added integration +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
+

+ Page level controls +

+
+
+ +
+
+
+
+
+ +
+
+
+

+ There is a new region landmark with page level controls at the end of the document. +

+
+ } + > + +
+ +

+ Page level controls +

+
+ +
+ +
+ + + + + +
+
+ +
+ +
+ +
+ + +
+ + + + + +
+
+
+ +
+
+ +

+ + There is a new region landmark with page level controls at the end of the document. + +

+
+ + + + +
+ +
+ + +`; + +exports[`Integration Setup Page Renders the data source form as expected 1`] = ` + +
+ +
+ + + (debug) Table detected + +
+
+ +
+ + +
+ +

+ Select index or data source for integration +

+
+ +
+ + +
+
+ + + No tables were found + +
+ +
+ +
+

+ No problem, we can help. Tell us about your data. +

+
+
+
+
+
+
+ +
+ + +
+ + + Use existing Data Source + +
+
+ +
+ + +
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+ +
+ +`; + +exports[`Integration Setup Page Renders the existing table form as expected 1`] = ` + +
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+ +
+ Manage data associated with this data source +
+
+
+
+
+ +
+ + + + +
+ +`; + +exports[`Integration Setup Page Renders the metadata form as expected 1`] = ` + + +
+ +

+ Name Integration +

+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ The name will be used to label the newly added integration +
+
+
+
+
+
+
+
+`; + +exports[`Integration Setup Page Renders the new table form as expected 1`] = ` + +
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/integrations/components/__tests__/setup_integration.test.tsx b/public/components/integrations/components/__tests__/setup_integration.test.tsx new file mode 100644 index 0000000000..61001ba7c5 --- /dev/null +++ b/public/components/integrations/components/__tests__/setup_integration.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { + SetupIntegrationDataSource, + SetupIntegrationExistingTable, + SetupIntegrationMetadata, + SetupIntegrationNewTable, + SetupIntegrationStepsPage, +} from '../setup_integration'; + +const TEST_CONFIG = { + instanceName: 'Test Instance Name', + useExisting: true, + dataSourceName: 'Test Datasource Name', + dataSourceDescription: 'Test Datasource Description', + dataSourceFileType: 'json', + dataSourceLocation: 'ss4o_logs-test-new-location', + existingDataSourceName: 'ss4o_logs-test-existing-location', +}; + +describe('Integration Setup Page', () => { + configure({ adapter: new Adapter() }); + + it('Renders integration setup page as expected', async () => { + const wrapper = mount(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders the metadata form as expected', async () => { + const wrapper = mount( + {}} /> + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders the data source form as expected', async () => { + const wrapper = mount( + {}} + showDataModal={false} + setShowDataModal={() => {}} + tableDetected={false} + setTableDetected={() => {}} + /> + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders the new table form as expected', async () => { + const wrapper = mount( + {}} /> + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders the existing table form as expected', async () => { + const wrapper = mount( + {}} + showDataModal={false} + setShowDataModal={() => {}} + /> + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/integrations/components/integration.tsx b/public/components/integrations/components/integration.tsx index 28257400b9..b7fba8c3ca 100644 --- a/public/components/integrations/components/integration.tsx +++ b/public/components/integrations/components/integration.tsx @@ -27,7 +27,6 @@ import { useToast } from '../../../../public/components/common/toast'; export function Integration(props: AvailableIntegrationProps) { const { http, integrationTemplateId, chrome } = props; - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const { setToast } = useToast(); const [integration, setIntegration] = useState({} as { name: string; type: string }); @@ -280,7 +279,7 @@ export function Integration(props: AvailableIntegrationProps) { {IntegrationOverview({ integration, showFlyout: () => { - setIsFlyoutVisible(true); + window.location.hash = `#/available/${integration.name}/setup`; }, setUpSample: () => { addIntegrationRequest(true, integrationTemplateId); @@ -299,19 +298,6 @@ export function Integration(props: AvailableIntegrationProps) { : IntegrationFields({ integration, integrationMapping })} - {isFlyoutVisible && ( - { - setIsFlyoutVisible(false); - }} - onCreate={(name, dataSource) => { - addIntegrationRequest(false, integrationTemplateId, name, dataSource); - }} - integrationName={integrationTemplateId} - integrationType={integration.type} - http={http} - /> - )} ); } diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx new file mode 100644 index 0000000000..9e3052abfb --- /dev/null +++ b/public/components/integrations/components/setup_integration.tsx @@ -0,0 +1,392 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Eui from '@elastic/eui'; +import { EuiContainedStepProps } from '@opensearch-project/oui/src/components/steps/steps'; +import React, { useState } from 'react'; + +interface IntegrationConfig { + instanceName: string; + useExisting: boolean; + dataSourceName: string; + dataSourceDescription: string; + dataSourceFileType: string; + dataSourceLocation: string; + existingDataSourceName: string; +} + +const STEPS: EuiContainedStepProps[] = [ + { title: 'Name Integration', children: }, + { title: 'Select index or data source for integration', children: }, +]; + +const ALLOWED_FILE_TYPES: Eui.EuiSelectOption[] = [ + { value: 'parquet', text: 'parquet' }, + { value: 'json', text: 'json' }, +]; + +const INTEGRATION_DATA_TABLE_COLUMNS = [ + { + field: 'field', + name: 'Field Name', + }, + { + field: 'type', + name: 'Field Type', + }, + { + field: 'isTimestamp', + name: 'Timestamp', + }, +]; + +const integrationDataTableData = [ + { + field: 'spanId', + type: 'string', + isTimestamp: false, + }, + { + field: 'severity.number', + type: 'long', + isTimestamp: false, + }, + { + field: '@timestamp', + type: 'date', + isTimestamp: true, + }, +]; + +const getSetupStepStatus = (activeStep: number): EuiContainedStepProps[] => { + return STEPS.map((step, idx) => { + let status: string = ''; + if (idx < activeStep) { + status = 'complete'; + } + if (idx > activeStep) { + status = 'disabled'; + } + return Object.assign({}, step, { status }); + }); +}; + +export function SetupIntegrationMetadata({ + name, + setName, +}: { + name: string; + setName: (name: string) => void; +}) { + return ( + + +

{STEPS[0].title}

+
+ + setName(evt.target.value)} /> + +
+ ); +} + +export function IntegrationDataModal({ close }: { close: () => void }): React.JSX.Element { + return ( + + +

Data Table

+
+ + + + + Close + + +
+ ); +} + +export function SetupIntegrationNewTable({ + config, + updateConfig, +}: { + config: IntegrationConfig; + updateConfig: (updates: Partial) => void; +}) { + return ( +
+ + updateConfig({ dataSourceName: evt.target.value })} + /> + + + updateConfig({ dataSourceDescription: evt.target.value })} + /> + + + updateConfig({ dataSourceFileType: evt.target.value })} + /> + + + updateConfig({ dataSourceLocation: evt.target.value })} + /> + +
+ ); +} + +export function SetupIntegrationExistingTable({ + config, + updateConfig, + showDataModal, + setShowDataModal, +}: { + config: IntegrationConfig; + updateConfig: (updates: Partial) => void; + showDataModal: boolean; + setShowDataModal: (visible: boolean) => void; +}) { + const dataModal = showDataModal ? ( + setShowDataModal(false)} /> + ) : null; + return ( +
+ + updateConfig({ existingDataSourceName: evt.target.value })} + /> + + + setShowDataModal(true)}>View table + {dataModal} +
+ ); +} + +export function SetupIntegrationDataSource({ + config, + updateConfig, + showDataModal, + setShowDataModal, + tableDetected, + setTableDetected, +}: { + config: IntegrationConfig; + updateConfig: (updates: Partial) => void; + showDataModal: boolean; + setShowDataModal: (show: boolean) => void; + tableDetected: boolean; + setTableDetected: (detected: boolean) => void; +}) { + let tableForm; + if (tableDetected && config.useExisting) { + tableForm = ( + setShowDataModal(x)} + /> + ); + } else { + tableForm = ; + } + + let tablesNotFoundMessage = null; + if (!tableDetected) { + tablesNotFoundMessage = ( + <> + +

No problem, we can help. Tell us about your data.

+
+ + + ); + } + + return ( +
+ setTableDetected(event.target.checked)} + /> + + + +

{STEPS[1].title}

+
+ + {tablesNotFoundMessage} + updateConfig({ useExisting: evt.target.checked })} + disabled={!tableDetected} + /> + + {tableForm} +
+
+ ); +} + +export function SetupIntegrationStep({ + activeStep, + config, + updateConfig, +}: { + activeStep: number; + config: IntegrationConfig; + updateConfig: (updates: Partial) => void; +}) { + const [isDataModalVisible, setDataModalVisible] = useState(false); + const [tableDetected, setTableDetected] = useState(false); + + switch (activeStep) { + case 0: + return ( + updateConfig({ instanceName: name })} + /> + ); + case 1: + return ( + setDataModalVisible(show)} + tableDetected={tableDetected} + setTableDetected={(detected: boolean) => setTableDetected(detected)} + /> + ); + default: + return ( + + Attempted to access integration setup step that doesn't exist. This is a bug. + + ); + } +} + +export function SetupBottomBar({ + step, + setStep, + config, +}: { + step: number; + setStep: (step: number) => void; + config: IntegrationConfig; +}) { + return ( + + + + { + // TODO evil hack because props aren't set up + let hash = window.location.hash; + hash = hash.trim(); + hash = hash.substring(0, hash.lastIndexOf('/setup')); + window.location.hash = hash; + }} + > + Cancel + + + + + + {step > 0 ? ( + + setStep(step - 1)}> + Back + + + ) : null} + + { + if (step < STEPS.length - 1) { + setStep(step + 1); + } else { + console.log(config); + } + }} + > + {step === STEPS.length - 1 ? 'Save' : 'Next'} + + + + + ); +} + +export function SetupIntegrationStepsPage() { + const [integConfig, setConfig] = useState({ + instanceName: '', + useExisting: true, + dataSourceName: '', + dataSourceDescription: '', + dataSourceFileType: 'parquet', + dataSourceLocation: '', + existingDataSourceName: '', + } as IntegrationConfig); + const [step, setStep] = useState(0); + + const updateConfig = (updates: Partial) => + setConfig(Object.assign({}, integConfig, updates)); + + return ( + + + + + + + + + + + setStep(Math.min(Math.max(x, 0), STEPS.length - 1))} + config={integConfig} + /> + + + ); +} diff --git a/public/components/integrations/home.tsx b/public/components/integrations/home.tsx index 3fa85b98dc..67ec7e74f1 100644 --- a/public/components/integrations/home.tsx +++ b/public/components/integrations/home.tsx @@ -13,6 +13,7 @@ import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { AvailableIntegrationOverviewPage } from './components/available_integration_overview_page'; import { AddedIntegrationOverviewPage } from './components/added_integration_overview_page'; import { AddedIntegration } from './components/added_integration'; +import { SetupIntegrationStepsPage } from './components/setup_integration'; export type AppAnalyticsCoreDeps = TraceAnalyticsCoreDeps; @@ -54,7 +55,7 @@ export const Home = (props: HomeProps) => { /> ( { /> )} /> + } />