{}}
+ onFileSelected={(_) => {}}
+ />
+ );
+
+ await waitFor(() => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
diff --git a/public/components/integrations/components/integration_details_panel.tsx b/public/components/integrations/components/integration_details_panel.tsx
index a69ffca76e..49a36e2ca1 100644
--- a/public/components/integrations/components/integration_details_panel.tsx
+++ b/public/components/integrations/components/integration_details_panel.tsx
@@ -15,6 +15,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import React from 'react';
+import { OPENSEARCH_CATALOG_URL } from '../../../../common/constants/integrations';
export function IntegrationDetails(props: { integration: IntegrationConfig }) {
const config = props.integration;
@@ -48,9 +49,7 @@ export function IntegrationDetails(props: { integration: IntegrationConfig }) {
-
- Check for new versions
-
+ Check for new versions
diff --git a/public/components/integrations/components/integration_header.tsx b/public/components/integrations/components/integration_header.tsx
index 72868134cd..fc823b95bb 100644
--- a/public/components/integrations/components/integration_header.tsx
+++ b/public/components/integrations/components/integration_header.tsx
@@ -4,9 +4,13 @@
*/
import {
+ EuiButton,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
EuiLink,
EuiPageHeader,
EuiPageHeaderSection,
+ EuiPopover,
EuiSpacer,
EuiTab,
EuiTabs,
@@ -14,9 +18,54 @@ import {
EuiTitle,
} from '@elastic/eui';
import React, { useState } from 'react';
-import { OPENSEARCH_DOCUMENTATION_URL } from '../../../../common/constants/integrations';
+import {
+ OPENSEARCH_CATALOG_URL,
+ OPENSEARCH_DOCUMENTATION_URL,
+} from '../../../../common/constants/integrations';
+import { IntegrationUploadFlyout } from './upload_flyout';
+
+export const IntegrationHeaderActions = ({ onShowUpload }: { onShowUpload: () => void }) => {
+ const [isPopoverOpen, setPopover] = useState(false);
+
+ const closePopover = () => {
+ setPopover(false);
+ };
+
+ const onButtonClick = () => {
+ setPopover((isOpen) => !isOpen);
+ };
+
+ const items = [
+ {
+ closePopover(); // If the popover isn't closed, it overlays over the flyout
+ onShowUpload();
+ }}
+ >
+ Upload Integrations
+ ,
+ View Catalog,
+ ];
+ const button = (
+
+ Catalog
+
+ );
+ return (
+
+ );
+};
-export function IntegrationHeader() {
+export const IntegrationHeader = () => {
const tabs = [
{
id: 'installed',
@@ -33,8 +82,9 @@ export function IntegrationHeader() {
const [selectedTabId, setSelectedTabId] = useState(
window.location.hash.substring(2) ? window.location.hash.substring(2) : 'installed'
);
+ const [showUploadFlyout, setShowUploadFlyout] = useState(false);
- const onSelectedTabChanged = (id) => {
+ const onSelectedTabChanged = (id: string) => {
setSelectedTabId(id);
window.location.hash = id;
};
@@ -51,6 +101,7 @@ export function IntegrationHeader() {
));
};
+
return (
@@ -59,6 +110,9 @@ export function IntegrationHeader() {
Integrations
+
+ setShowUploadFlyout(true)} />
+
@@ -70,6 +124,9 @@ export function IntegrationHeader() {
{renderTabs()}
+ {showUploadFlyout ? (
+ setShowUploadFlyout(false)} />
+ ) : null}
);
-}
+};
diff --git a/public/components/integrations/components/upload_flyout.tsx b/public/components/integrations/components/upload_flyout.tsx
new file mode 100644
index 0000000000..e9d3a15da5
--- /dev/null
+++ b/public/components/integrations/components/upload_flyout.tsx
@@ -0,0 +1,174 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiButton,
+ EuiFilePicker,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiForm,
+ EuiFormRow,
+} from '@elastic/eui';
+import { useEffect } from 'react';
+import { coreRefs } from '../../../../public/framework/core_refs';
+import { useToast } from '../../../../public/components/common/toast';
+
+const uploadBundle = async (bundle: File | null): Promise => {
+ if (!bundle) {
+ return new Error('No bundle selected');
+ }
+
+ const formData = new FormData();
+ formData.append('file', bundle);
+ try {
+ const response = await coreRefs.http?.post('/api/saved_objects/_import', {
+ headers: {
+ // Important to leave undefined, it forces proper headers to be set for FormData
+ 'Content-Type': undefined,
+ },
+ body: formData,
+ query: {
+ overwrite: true,
+ // If we don't disable this, we'll get 413 Content Too Large errors for many bundles
+ dataSourceEnabled: false,
+ },
+ });
+
+ console.log(response);
+ if (response.success) {
+ return null;
+ } else {
+ return new Error(response.body.message);
+ }
+ } catch (err) {
+ return err;
+ }
+};
+
+const checkBundle = async (bundle: File | null): Promise => {
+ if (bundle == null) {
+ return false;
+ }
+ if (!/.*\.ndjson&/i.test(bundle.name) && bundle.type !== 'application/x-ndjson') {
+ return false;
+ }
+
+ try {
+ const contents = await bundle.text();
+ const objects = contents
+ .trim()
+ .split('\n')
+ .map((s) => JSON.parse(s));
+ return objects.every((obj) => !obj.type || obj.type === 'integration-template');
+ } catch (err) {
+ return false;
+ }
+};
+
+export const IntegrationUploadPicker = ({
+ onFileSelected,
+ isInvalid,
+ setIsInvalid,
+}: {
+ onFileSelected: (file: File | null) => void;
+ isInvalid: boolean;
+ setIsInvalid: (value: boolean) => void;
+}) => {
+ const [blurred, setBlurred] = useState(false);
+ const [checkCompleted, setCheckCompleted] = useState(false);
+ const [bundle, setBundle] = useState(null as File | null);
+
+ useEffect(() => {
+ const refreshValidity = async () => {
+ if (!blurred) {
+ return;
+ }
+ const valid = await checkBundle(bundle);
+ setIsInvalid(!valid);
+ setCheckCompleted(true);
+ };
+ setCheckCompleted(false);
+ refreshValidity();
+ // It's important that we update *only* on the bundle changing here, so we don't report errors
+ // early when the file picker is still open and get error flickering on the UI in the happy path.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [bundle]);
+
+ const handleFileChange = (files: FileList | null) => {
+ if (files && files.length > 0) {
+ setBundle(files[0]);
+ onFileSelected(files[0]);
+ } else {
+ setBundle(null);
+ onFileSelected(null);
+ }
+ };
+
+ return (
+
+
+ setBlurred(true)}
+ />
+
+
+ );
+};
+
+export const IntegrationUploadFlyout = ({ onClose }: { onClose: () => void }) => {
+ const [bundle, setBundle] = useState(null as File | null);
+ const { setToast } = useToast();
+ const [isInvalid, setIsInvalid] = useState(true);
+
+ return (
+
+ Upload Integrations
+
+
+
+
+
+
+ Cancel
+
+
+ {
+ const err = await uploadBundle(bundle);
+ if (err == null) {
+ setToast('Successfully uploaded bundle', 'success');
+ onClose();
+ } else {
+ setToast('Error uploading bundle', 'danger', err.message ?? '');
+ }
+ }}
+ >
+ Upload
+
+
+
+
+
+ );
+};