Skip to content

Commit

Permalink
[Visualize] Renders no data component if there is no ES data or datav…
Browse files Browse the repository at this point in the history
…iew (#132223)

* [Visualize] Renders no data component if there is no ES data or dataview

* Fix types

* Adds a functional test

* Fix FTs

* Fix

* Fix no data test

* Changes on the dashboard save test

* Add a loader before the data being fetched

* Add check for default dataview

* A small nit

* Address PR comments
  • Loading branch information
stratoula authored May 18, 2022
1 parent 9aac8b8 commit 465c419
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/plugins/visualizations/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"savedObjects",
"screenshotMode",
"presentationUtil",
"dataViews"
"dataViews",
"dataViewEditor"
],
"optionalPlugins": ["home", "share", "usageCollection", "spaces", "savedObjectsTaggingOss"],
"requiredBundles": ["kibanaUtils", "discover", "kibanaReact"],
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/visualizations/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
Expand Down Expand Up @@ -58,6 +59,7 @@ const createInstance = async () => {
plugin.start(coreMock.createStart(), {
data: dataPluginMock.createStartContract(),
dataViews: dataViewPluginMocks.createStartContract(),
dataViewEditor: indexPatternEditorPluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
inspector: inspectorPluginMock.createStartContract(),
uiActions: uiActionsPluginMock.createStartContract(),
Expand Down
7 changes: 4 additions & 3 deletions src/plugins/visualizations/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { TypesSetup, TypesStart } from './vis_types';
import type { VisualizeServices } from './visualize_app/types';
import { visualizeEditorTrigger } from './triggers';
Expand Down Expand Up @@ -118,6 +119,7 @@ export interface VisualizationsSetupDeps {
export interface VisualizationsStartDeps {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
expressions: ExpressionsStart;
embeddable: EmbeddableStart;
inspector: InspectorStart;
Expand Down Expand Up @@ -239,9 +241,6 @@ export class VisualizationsPlugin

// make sure the index pattern list is up to date
pluginsStart.dataViews.clearCache();
// make sure a default index pattern exists
// if not, the page will be redirected to management and visualize won't be rendered
await pluginsStart.dataViews.ensureDefaultDataView();

appMounted();

Expand Down Expand Up @@ -269,6 +268,8 @@ export class VisualizationsPlugin
pluginInitializerContext: this.initializerContext,
chrome: coreStart.chrome,
data: pluginsStart.data,
core: coreStart,
dataViewEditor: pluginsStart.dataViewEditor,
dataViews: pluginsStart.dataViews,
localStorage: new Storage(localStorage),
navigation: pluginsStart.navigation,
Expand Down
7 changes: 7 additions & 0 deletions src/plugins/visualizations/public/visualize_app/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@
flex-direction: column;
flex-grow: 1;
}

.visAppLoadingWrapper {
align-items: center;
justify-content: center;
display: flex;
flex-grow: 1;
}
89 changes: 85 additions & 4 deletions src/plugins/visualizations/public/visualize_app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
*/

import './app.scss';
import React, { useEffect } from 'react';
import React, { useEffect, useCallback, useState } from 'react';
import { Route, Switch, useLocation } from 'react-router-dom';

import { AppMountParameters } from '@kbn/core/public';
import { EuiLoadingSpinner } from '@elastic/eui';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { syncQueryStateWithUrl } from '@kbn/data-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
AnalyticsNoDataPageKibanaProvider,
AnalyticsNoDataPage,
} from '@kbn/shared-ux-page-analytics-no-data';
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
import { VisualizeServices } from './types';
import {
VisualizeEditor,
Expand All @@ -26,14 +32,49 @@ export interface VisualizeAppProps {
onAppLeave: AppMountParameters['onAppLeave'];
}

interface NoDataComponentProps {
core: CoreStart;
dataViews: DataViewsContract;
dataViewEditor: DataViewEditorStart;
onDataViewCreated: (dataView: unknown) => void;
}

const NoDataComponent = ({
core,
dataViews,
dataViewEditor,
onDataViewCreated,
}: NoDataComponentProps) => {
const analyticsServices = {
coreStart: core,
dataViews,
dataViewEditor,
};
return (
<AnalyticsNoDataPageKibanaProvider {...analyticsServices}>
<AnalyticsNoDataPage onDataViewCreated={onDataViewCreated} />;
</AnalyticsNoDataPageKibanaProvider>
);
};

export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => {
const {
services: {
data: { query },
data: { query, dataViews },
core,
kbnUrlStateStorage,
dataViewEditor,
},
} = useKibana<VisualizeServices>();
const { pathname } = useLocation();
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);

const onDataViewCreated = useCallback((dataView: unknown) => {
if (dataView) {
setShowNoDataPage(false);
}
}, []);

useEffect(() => {
// syncs `_g` portion of url with query services
Expand All @@ -45,6 +86,46 @@ export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => {
// so the global state is always preserved
}, [query, kbnUrlStateStorage, pathname]);

useEffect(() => {
const checkESOrDataViewExist = async () => {
// check if there is any data view or data source
const hasUserDataView = await dataViews.hasData.hasUserDataView().catch(() => false);
const hasEsData = await dataViews.hasData.hasESData().catch(() => false);
if (!hasUserDataView || !hasEsData) {
setShowNoDataPage(true);
}
// Adding this check as TSVB asks for the default dataview on initialization
const defaultDataView = await dataViews.getDefaultDataView();
if (!defaultDataView) {
setShowNoDataPage(true);
}
setIsLoading(false);
};

// call the function
checkESOrDataViewExist();
}, [dataViews]);

if (isLoading) {
return (
<div className="visAppLoadingWrapper">
<EuiLoadingSpinner size="xl" />
</div>
);
}

// Visualize app should return the noData component if there is no data view or data source
if (showNoDataPage) {
return (
<NoDataComponent
core={core}
dataViewEditor={dataViewEditor}
dataViews={dataViews}
onDataViewCreated={onDataViewCreated}
/>
);
}

return (
<Switch>
<Route exact path={`${VisualizeConstants.EDIT_BY_VALUE_PATH}`}>
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/visualizations/public/visualize_app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
IKbnUrlStateStorage,
ReduxLikeStateContainer,
} from '@kbn/kibana-utils-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';

import type { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public';
import type { Filter } from '@kbn/es-query';
Expand Down Expand Up @@ -85,7 +86,9 @@ export interface VisualizeServices extends CoreStart {
stateTransferService: EmbeddableStateTransfer;
embeddable: EmbeddableStart;
history: History;
dataViewEditor: DataViewEditorStart;
kbnUrlStateStorage: IKbnUrlStateStorage;
core: CoreStart;
urlForwarding: UrlForwardingStart;
pluginInitializerContext: PluginInitializerContext;
chrome: ChromeStart;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/visualizations/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
{ "path": "../navigation/tsconfig.json" },
{ "path": "../home/tsconfig.json" },
{ "path": "../share/tsconfig.json" },
{ "path": "../data_view_editor/tsconfig.json" },
{ "path": "../presentation_util/tsconfig.json" },
{ "path": "../screenshot_mode/tsconfig.json" },
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" }
Expand Down
2 changes: 2 additions & 0 deletions test/functional/apps/dashboard/group4/dashboard_save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const listingTable = getService('listingTable');
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const esArchiver = getService('esArchiver');

describe('dashboard save', function describeIndexTests() {
this.tags('includeFirefox');
Expand Down Expand Up @@ -125,6 +126,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});

it('Does not show dashboard save modal when on quick save', async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.saveDashboard('test quick save');
Expand Down
67 changes: 67 additions & 0 deletions test/functional/apps/visualize/group1/_no_data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';

export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['visualize', 'header', 'common']);
const esArchiver = getService('esArchiver');
const find = getService('find');
const kibanaServer = getService('kibanaServer');

const createDataView = async (dataViewName: string) => {
await testSubjects.setValue('createIndexPatternNameInput', dataViewName, {
clearWithKeyboard: true,
typeCharByChar: true,
});
await testSubjects.click('saveIndexPatternButton');
};

describe('no data in visualize', function () {
it('should show the integrations component if there is no data', async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
await esArchiver.unload('test/functional/fixtures/es_archiver/long_window_logstash');
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await PageObjects.common.navigateToApp('visualize');
await PageObjects.header.waitUntilLoadingHasFinished();

const addIntegrations = await testSubjects.find('kbnOverviewAddIntegrations');
await addIntegrations.click();
await PageObjects.common.waitUntilUrlIncludes('integrations/browse');
});

it('should show the no dataview component if no dataviews exist', async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash');
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await PageObjects.common.navigateToApp('visualize');
await PageObjects.header.waitUntilLoadingHasFinished();
const button = await testSubjects.find('createDataViewButtonFlyout');
button.click();
await retry.waitForWithTimeout('index pattern editor form to be visible', 15000, async () => {
return await (await find.byClassName('indexPatternEditor__form')).isDisplayed();
});

const dataViewToCreate = 'logstash';
await createDataView(dataViewToCreate);
await PageObjects.header.waitUntilLoadingHasFinished();

await retry.waitForWithTimeout(
'data view selector to include a newly created dataview',
5000,
async () => {
const addNewVizButton = await testSubjects.exists('newItemButton');
expect(addNewVizButton).to.be(true);
return addNewVizButton;
}
);
});
});
}
2 changes: 1 addition & 1 deletion test/functional/apps/visualize/group1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash');
});

loadTestFile(require.resolve('./_embedding_chart'));
loadTestFile(require.resolve('./_data_table'));
loadTestFile(require.resolve('./_data_table_nontimeindex'));
loadTestFile(require.resolve('./_data_table_notimeindex_filters'));
loadTestFile(require.resolve('./_chart_types'));
loadTestFile(require.resolve('./_no_data'));
});
}
22 changes: 15 additions & 7 deletions x-pack/test/functional/apps/maps/group4/visualize_create_menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ export default function ({ getService, getPageObjects }) {
describe('maps visualize alias', () => {
describe('with write permission', () => {
before(async () => {
await security.testUser.setRoles(['global_maps_all', 'global_visualize_all'], {
skipBrowserRefresh: true,
});
await security.testUser.setRoles(
['global_maps_all', 'global_visualize_all', 'test_logstash_reader'],
{
skipBrowserRefresh: true,
}
);

await PageObjects.visualize.navigateToNewVisualization();
});
Expand All @@ -38,9 +41,12 @@ export default function ({ getService, getPageObjects }) {

describe('without write permission', () => {
before(async () => {
await security.testUser.setRoles(['global_maps_read', 'global_visualize_all'], {
skipBrowserRefresh: true,
});
await security.testUser.setRoles(
['global_maps_read', 'global_visualize_all', 'test_logstash_reader'],
{
skipBrowserRefresh: true,
}
);

await PageObjects.visualize.navigateToNewVisualization();
});
Expand All @@ -58,7 +64,9 @@ export default function ({ getService, getPageObjects }) {

describe('aggregion based visualizations', () => {
before(async () => {
await security.testUser.setRoles(['global_visualize_all'], { skipBrowserRefresh: true });
await security.testUser.setRoles(['global_visualize_all', 'test_logstash_reader'], {
skipBrowserRefresh: true,
});

await PageObjects.visualize.navigateToNewAggBasedVisualization();
});
Expand Down

0 comments on commit 465c419

Please sign in to comment.