diff --git a/common/constants/shared.ts b/common/constants/shared.ts index ee64aa169a..6469c0f3d0 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -35,6 +35,7 @@ export const JOBS_ENDPOINT_BASE = '/_plugins/_async_query'; export const JOB_RESULT_ENDPOINT = '/result'; export const tutorialSampleDataPluginId = 'import_sample_data'; +export const dataSourceManagementPluginId = 'dataSources'; export const observabilityID = 'observability-logs'; export const observabilityTitle = 'Observability'; diff --git a/public/components/overview/components/__tests__/__snapshots__/add_datasource_callout.test.tsx.snap b/public/components/overview/components/__tests__/__snapshots__/add_datasource_callout.test.tsx.snap new file mode 100644 index 0000000000..db049b7140 --- /dev/null +++ b/public/components/overview/components/__tests__/__snapshots__/add_datasource_callout.test.tsx.snap @@ -0,0 +1,179 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Add dashboard callout renders add datasource callout 1`] = ` + + +
+ +
+ +
+ + + + + + + +
+
+ +
+ +

+ No connected data sources +

+
+
+
+ +
+ +
+ +
+

+ There are no data sources associated to the workspace. Associate data sources or request your administrator to associate data sources for you to get started. +

+
+
+
+
+
+
+ +
+ + + + + +
+
+
+
+
+
+
+`; diff --git a/public/components/overview/components/__tests__/__snapshots__/dashboard_controls.test.tsx.snap b/public/components/overview/components/__tests__/__snapshots__/dashboard_controls.test.tsx.snap index ca777e4441..41fe8c20ef 100644 --- a/public/components/overview/components/__tests__/__snapshots__/dashboard_controls.test.tsx.snap +++ b/public/components/overview/components/__tests__/__snapshots__/dashboard_controls.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Dashboard controls should render 1`] = ` +exports[`Dashboard controls - checkDataSource useEffect simplified should render 1`] = ` { + configure({ adapter: new Adapter() }); + + const wrapper = mount(); + + it('renders add datasource callout', async () => { + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/overview/components/__tests__/dashboard_controls.test.tsx b/public/components/overview/components/__tests__/dashboard_controls.test.tsx index f51d8c7b79..e153bbe1b8 100644 --- a/public/components/overview/components/__tests__/dashboard_controls.test.tsx +++ b/public/components/overview/components/__tests__/dashboard_controls.test.tsx @@ -7,32 +7,92 @@ import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { DashboardControls } from '../dashboard_controls'; +import { coreRefs } from '../../../../framework/core_refs'; +import { setObservabilityDashboardsId } from '../utils'; +import { getWorkspaceIdFromUrl } from '../../../../../../../src/core/public/utils'; +import { act } from 'react-dom/test-utils'; configure({ adapter: new Adapter() }); -const mountDashboardControls = () => { - return mount(); -}; - jest.mock('../../../getting_started/components/utils', () => ({ redirectToDashboards: jest.fn(), })); jest.mock('../../../../framework/core_refs', () => ({ coreRefs: { - contentManagement: { - updatePageSection: jest.fn(), + savedObjectsClient: { + find: jest.fn(), + }, + http: { + basePath: { + getBasePath: jest.fn(() => '/basePath'), + }, }, }, })); -describe('Dashboard controls', () => { +jest.mock('../utils', () => ({ + setObservabilityDashboardsId: jest.fn(), +})); + +jest.mock('../../../../../common/utils', () => ({ + getOverviewPage: jest.fn(() => ({ + removeSection: jest.fn(), + })), +})); + +jest.mock('../../../../../../../src/core/public/utils', () => ({ + getWorkspaceIdFromUrl: jest.fn(), +})); + +describe('Dashboard controls - checkDataSource useEffect simplified', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render', () => { - const wrapper = mountDashboardControls(); + const wrapper = mount(); expect(wrapper).toMatchSnapshot(); }); + + it('should handle no data sources in a workspace', async () => { + getWorkspaceIdFromUrl.mockReturnValue('workspace123'); + coreRefs.savedObjectsClient.find.mockResolvedValue({ savedObjects: [] }); + + const mockSetObservabilityDashboardsId = setObservabilityDashboardsId; + + await act(async () => { + mount(); + await new Promise((resolve) => setImmediate(resolve)); + }); + + expect(mockSetObservabilityDashboardsId).toHaveBeenCalledWith(null); + }); + + it('should handle existing data sources in a workspace', async () => { + getWorkspaceIdFromUrl.mockReturnValue('workspace123'); + coreRefs.savedObjectsClient.find.mockResolvedValue({ savedObjects: [{ id: 'ds1' }] }); + + const mockSetObservabilityDashboardsId = setObservabilityDashboardsId; + + await act(async () => { + mount(); + await new Promise((resolve) => setImmediate(resolve)); + }); + + expect(mockSetObservabilityDashboardsId).not.toHaveBeenCalled(); + }); + + it('should handle non-workspace scenario', async () => { + getWorkspaceIdFromUrl.mockReturnValue(null); + + const mockSetObservabilityDashboardsId = setObservabilityDashboardsId; + + await act(async () => { + mount(); + await new Promise((resolve) => setImmediate(resolve)); + }); + + expect(mockSetObservabilityDashboardsId).not.toHaveBeenCalled(); + }); }); diff --git a/public/components/overview/components/add_dashboard_callout.tsx b/public/components/overview/components/add_dashboard_callout.tsx index d8600da491..bab90c4b24 100644 --- a/public/components/overview/components/add_dashboard_callout.tsx +++ b/public/components/overview/components/add_dashboard_callout.tsx @@ -26,7 +26,7 @@ import SampleDataLightPNG from './assets/SampleDataLight.png'; export function AddDashboardCallout() { const showFlyout = useObservable(ObsDashboardStateManager.showFlyout$); - const isDarkMode = uiSettingsService.get('theme:darkMode'); + const isDarkMode = uiSettingsService?.get('theme:darkMode') ?? false; return ( diff --git a/public/components/overview/components/add_datasource_callout.tsx b/public/components/overview/components/add_datasource_callout.tsx new file mode 100644 index 0000000000..fa7b47a772 --- /dev/null +++ b/public/components/overview/components/add_datasource_callout.tsx @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiIcon, + EuiTitle, + EuiText, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { coreRefs } from '../../../framework/core_refs'; +import { dataSourceManagementPluginId } from '../../../../common/constants/shared'; + +export function AddDataSourceCallout() { + return ( + + + + + + + +

No connected data sources

+
+
+ + +

+ {i18n.translate('traceAnalytics.noDataSourcesMessage', { + defaultMessage: + 'There are no data sources associated to the workspace. Associate data sources or request your administrator to associate data sources for you to get started.', + })} +

+
+
+ + + coreRefs.application?.navigateToApp(dataSourceManagementPluginId, { path: '#/' }) + } + > + Manage data sources + + +
+
+ ); +} diff --git a/public/components/overview/components/dashboard_controls.tsx b/public/components/overview/components/dashboard_controls.tsx index 110afb2a60..c513b92d36 100644 --- a/public/components/overview/components/dashboard_controls.tsx +++ b/public/components/overview/components/dashboard_controls.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React, { useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -13,19 +14,59 @@ import { EuiToolTip, } from '@elastic/eui'; import { OnTimeChangeProps } from '@opensearch-project/oui/src/eui_components/date_picker/super_date_picker/super_date_picker'; -import React from 'react'; import { useObservable } from 'react-use'; import { FormattedMessage } from '@osd/i18n/react'; import { coreRefs } from '../../../framework/core_refs'; -import { HOME_CONTENT_AREAS } from '../../../plugin_helpers/plugin_overview'; +import { HOME_CONTENT_AREAS, SECTIONS } from '../../../plugin_helpers/plugin_overview'; import { redirectToDashboards } from '../../getting_started/components/utils'; import { AddDashboardCallout } from './add_dashboard_callout'; +import { AddDataSourceCallout } from './add_datasource_callout'; import { ObsDashboardStateManager } from './obs_dashboard_state_manager'; +import { SavedObjectsClientCommonFindArgs } from '../../../../../../src/plugins/data/common'; +import { getWorkspaceIdFromUrl } from '../../../../../../src/core/public/utils'; +import { setObservabilityDashboardsId } from './utils'; +import { getOverviewPage } from '../../../../common/utils'; + +const getDatasourceAttributes = async () => { + const findOptions: SavedObjectsClientCommonFindArgs = { + type: 'data-source', + perPage: 1000, + }; + + const allDataSources = await coreRefs?.savedObjectsClient?.find(findOptions); + return allDataSources?.savedObjects ?? []; +}; export function DashboardControls() { + const [isDataSourceEmpty, setIsDataSourceEmpty] = useState(null); + const [isInWorkspace, setIsInWorkspace] = useState(false); // Track if user is in a workspace const isDashboardSelected = useObservable(ObsDashboardStateManager.isDashboardSelected$); const dashboardState = useObservable(ObsDashboardStateManager.dashboardState$); + const checkDataSource = async () => { + const currentUrl = window.location.href; + const workspaceId = getWorkspaceIdFromUrl(currentUrl, coreRefs?.http?.basePath.getBasePath()); + + if (workspaceId) { + setIsInWorkspace(true); + const savedObjectsArray = await getDatasourceAttributes(); + setIsDataSourceEmpty(savedObjectsArray.length === 0); + + // Set to null if there are no data sources associated [Handle if dashboard was set, then datasource deleted] + if (savedObjectsArray.length === 0) { + await setObservabilityDashboardsId(null); + getOverviewPage().removeSection(SECTIONS.DASHBOARD); // Clear the present dashboard + } + } else { + setIsInWorkspace(false); + setIsDataSourceEmpty(false); // Not in workspace + } + }; + + useEffect(() => { + checkDataSource(); + }, []); + const onTimeChange = (onTimeChangeProps: OnTimeChangeProps) => { ObsDashboardStateManager.dashboardState$.next({ ...dashboardState!, @@ -47,6 +88,11 @@ export function DashboardControls() { }); }; + // Directly show AddDataSourceCallout if in workspace and no data source is associated + if (isInWorkspace && isDataSourceEmpty) { + return ; + } + return isDashboardSelected ? ( diff --git a/public/components/overview/components/select_dashboard_flyout.tsx b/public/components/overview/components/select_dashboard_flyout.tsx index eb914aaedd..1d01b90130 100644 --- a/public/components/overview/components/select_dashboard_flyout.tsx +++ b/public/components/overview/components/select_dashboard_flyout.tsx @@ -26,6 +26,8 @@ import { ObsDashboardStateManager } from './obs_dashboard_state_manager'; import { getObservabilityDashboardsId, setObservabilityDashboardsId } from './utils'; import { coreRefs } from '../../../framework/core_refs'; import { tutorialSampleDataPluginId } from '../../../../common/constants/shared'; +import { getOverviewPage } from '../../../../common/utils'; +import { DASHBOARD_SECTION } from '../../../../public/plugin_helpers/plugin_overview'; export interface Props { closeFlyout: () => void; @@ -61,6 +63,7 @@ export function SelectDashboardFlyout({ closeFlyout, dashboardsSavedObjects, rel const onClickAdd = async () => { const selectedOption = options.find((option) => option.checked === 'on'); if (selectedOption && selectedOption.key) { + getOverviewPage().createSection(DASHBOARD_SECTION); setIsLoading(true); ObsDashboardStateManager.isDashboardSelected$.next(true); await setObservabilityDashboardsId(selectedOption.key); diff --git a/public/components/overview/home.tsx b/public/components/overview/home.tsx index 66256500ae..b21aa3d424 100644 --- a/public/components/overview/home.tsx +++ b/public/components/overview/home.tsx @@ -10,6 +10,9 @@ import { EuiIcon, EuiButtonEmpty, EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, } from '@elastic/eui'; import React, { ReactNode, useEffect, useMemo, useState } from 'react'; import { HashRouter, Route, Switch } from 'react-router-dom'; @@ -51,6 +54,7 @@ export const Home = () => { const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [showGetStarted, setShowGetStarted] = useState(null); // Initial null state + const [isDashboardLoaded, setIsDashboardLoaded] = useState(false); const canUpdateUiSetting = useMemo(() => { const capabilities = coreRefs.application?.capabilities; @@ -74,6 +78,7 @@ export const Home = () => { const closePopover = () => setIsPopoverOpen(false); const loadDashboardState = () => { + setIsDashboardLoaded(false); coreRefs.savedObjectsClient ?.find({ type: 'dashboard', @@ -121,6 +126,9 @@ export const Home = () => { }) .catch((error) => { console.error('Error fetching dashboards:', error); + }) + .finally(() => { + setIsDashboardLoaded(true); }); }; @@ -175,7 +183,6 @@ export const Home = () => { closePopover(); if (updatedShowCards) { - console.log('Called createSection'); getOverviewPage().createSection(GET_STARTED_SECTION); } else { getOverviewPage().removeSection(SECTIONS.GET_STARTED); @@ -382,8 +389,22 @@ export const Home = () => {
- {homePage} - {flyout} + {isDashboardLoaded ? ( + <> + {homePage} + {flyout} + + ) : ( + + + + + + )}
diff --git a/public/plugin_helpers/plugin_overview.tsx b/public/plugin_helpers/plugin_overview.tsx index cb9571babc..a184826250 100644 --- a/public/plugin_helpers/plugin_overview.tsx +++ b/public/plugin_helpers/plugin_overview.tsx @@ -32,23 +32,24 @@ export const GET_STARTED_SECTION: Section = { columns: 5, }; +export const SELECTOR_SECTION: Section = { + id: SECTIONS.SELECTOR, + order: 2000, + title: 'Dashboards controls', + kind: 'custom', + render: (contents) =>
{contents[0].render()}
, +}; + +export const DASHBOARD_SECTION: Section = { + id: SECTIONS.DASHBOARD, + order: 3000, + kind: 'dashboard', +}; + export const setupOverviewPage = (contentManagement: ContentManagementPluginSetup) => { return contentManagement.registerPage({ id: HOME_PAGE_ID, title: 'Home', - sections: [ - { - id: SECTIONS.SELECTOR, - order: 2000, - title: 'Dashboards controls', - kind: 'custom', - render: (contents) =>
{contents[0].render()}
, - }, - { - id: SECTIONS.DASHBOARD, - order: 3000, - kind: 'dashboard', - }, - ], + sections: [SELECTOR_SECTION, DASHBOARD_SECTION], }); };