mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Enable right click on visualizations and dashboards listings (#88936)
* Enable right-click on visualizations listing page * Make it simpler * Enable right click to the dashboard listing * Add unit tests * Fix link on dashboard * Fix visualize link * Fix PR comments * Fix functional test failure * Use kbnUrlStateStorage instead * Change method to getDashboardListItemLink * Change method to getVisualizeListItemLink Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8534faf7ee
commit
b4931e6f5e
9 changed files with 370 additions and 31 deletions
|
@ -9,16 +9,15 @@
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React, { Fragment, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { attemptLoadDashboardByTitle } from '../lib';
|
||||
import { DashboardAppServices, DashboardRedirect } from '../types';
|
||||
import { getDashboardBreadcrumb, dashboardListingTable } from '../../dashboard_strings';
|
||||
import { ApplicationStart, SavedObjectsFindOptionsReference } from '../../../../../core/public';
|
||||
|
||||
import { syncQueryStateWithUrl } from '../../services/data';
|
||||
import { IKbnUrlStateStorage } from '../../services/kibana_utils';
|
||||
import { TableListView, useKibana } from '../../services/kibana_react';
|
||||
import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss';
|
||||
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
|
||||
|
||||
export interface DashboardListingProps {
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
|
@ -83,8 +82,13 @@ export const DashboardListing = ({
|
|||
|
||||
const tableColumns = useMemo(
|
||||
() =>
|
||||
getTableColumns((id) => redirectTo({ destination: 'dashboard', id }), savedObjectsTagging),
|
||||
[savedObjectsTagging, redirectTo]
|
||||
getTableColumns(
|
||||
core.application,
|
||||
kbnUrlStateStorage,
|
||||
core.uiSettings.get('state:storeInSessionStorage'),
|
||||
savedObjectsTagging
|
||||
),
|
||||
[core.application, core.uiSettings, kbnUrlStateStorage, savedObjectsTagging]
|
||||
);
|
||||
|
||||
const noItemsFragment = useMemo(
|
||||
|
@ -99,7 +103,6 @@ export const DashboardListing = ({
|
|||
(filter: string) => {
|
||||
let searchTerm = filter;
|
||||
let references: SavedObjectsFindOptionsReference[] | undefined;
|
||||
|
||||
if (savedObjectsTagging) {
|
||||
const parsed = savedObjectsTagging.ui.parseSearchQuery(filter, {
|
||||
useName: true,
|
||||
|
@ -164,7 +167,9 @@ export const DashboardListing = ({
|
|||
};
|
||||
|
||||
const getTableColumns = (
|
||||
redirectTo: (id?: string) => void,
|
||||
application: ApplicationStart,
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage,
|
||||
useHash: boolean,
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi
|
||||
) => {
|
||||
return [
|
||||
|
@ -172,9 +177,15 @@ const getTableColumns = (
|
|||
field: 'title',
|
||||
name: dashboardListingTable.getTitleColumnName(),
|
||||
sortable: true,
|
||||
render: (field: string, record: { id: string; title: string }) => (
|
||||
render: (field: string, record: { id: string; title: string; timeRestore: boolean }) => (
|
||||
<EuiLink
|
||||
onClick={() => redirectTo(record.id)}
|
||||
href={getDashboardListItemLink(
|
||||
application,
|
||||
kbnUrlStateStorage,
|
||||
useHash,
|
||||
record.id,
|
||||
record.timeRestore
|
||||
)}
|
||||
data-test-subj={`dashboardListingTitleLink-${record.title.split(' ').join('-')}`}
|
||||
>
|
||||
{field}
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { esFilters } from '../../../../data/public';
|
||||
import { createHashHistory } from 'history';
|
||||
import { createKbnUrlStateStorage } from '../../../../kibana_utils/public';
|
||||
import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator';
|
||||
|
||||
const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647';
|
||||
|
||||
const application = ({
|
||||
getUrlForApp: jest.fn((appId: string, options?: { path?: string; absolute?: boolean }) => {
|
||||
return `/app/${appId}${options?.path}`;
|
||||
}),
|
||||
} as unknown) as ApplicationStart;
|
||||
|
||||
const history = createHashHistory();
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
});
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } });
|
||||
|
||||
describe('listing dashboard link', () => {
|
||||
test('creates a link to a dashboard without the timerange query if time is saved on the dashboard', async () => {
|
||||
const url = getDashboardListItemLink(
|
||||
application,
|
||||
kbnUrlStateStorage,
|
||||
false,
|
||||
DASHBOARD_ID,
|
||||
true
|
||||
);
|
||||
expect(url).toMatchInlineSnapshot(`"/app/dashboards#/view/${DASHBOARD_ID}?_g=()"`);
|
||||
});
|
||||
|
||||
test('creates a link to a dashboard with the timerange query if time is not saved on the dashboard', async () => {
|
||||
const url = getDashboardListItemLink(
|
||||
application,
|
||||
kbnUrlStateStorage,
|
||||
false,
|
||||
DASHBOARD_ID,
|
||||
false
|
||||
);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:now-7d,to:now))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when global time changes', () => {
|
||||
beforeEach(() => {
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
|
||||
time: {
|
||||
from: '2021-01-05T11:45:53.375Z',
|
||||
to: '2021-01-21T11:46:00.990Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('propagates the correct time on the query', async () => {
|
||||
const url = getDashboardListItemLink(
|
||||
application,
|
||||
kbnUrlStateStorage,
|
||||
false,
|
||||
DASHBOARD_ID,
|
||||
false
|
||||
);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when global refreshInterval changes', () => {
|
||||
beforeEach(() => {
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
|
||||
refreshInterval: { pause: false, value: 300 },
|
||||
});
|
||||
});
|
||||
|
||||
test('propagates the refreshInterval on the query', async () => {
|
||||
const url = getDashboardListItemLink(
|
||||
application,
|
||||
kbnUrlStateStorage,
|
||||
false,
|
||||
DASHBOARD_ID,
|
||||
false
|
||||
);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/dashboards#/view/${DASHBOARD_ID}?_g=(refreshInterval:(pause:!f,value:300))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when global filters change', () => {
|
||||
beforeEach(() => {
|
||||
const filters = [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
query: { query: 'q1' },
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
query: { query: 'q1' },
|
||||
$state: {
|
||||
store: esFilters.FilterStateStore.GLOBAL_STATE,
|
||||
},
|
||||
},
|
||||
];
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
|
||||
filters,
|
||||
});
|
||||
});
|
||||
|
||||
test('propagates the filters on the query', async () => {
|
||||
const url = getDashboardListItemLink(
|
||||
application,
|
||||
kbnUrlStateStorage,
|
||||
false,
|
||||
DASHBOARD_ID,
|
||||
false
|
||||
);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/dashboards#/view/${DASHBOARD_ID}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { QueryState } from '../../../../data/public';
|
||||
import { setStateToKbnUrl } from '../../../../kibana_utils/public';
|
||||
import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants';
|
||||
import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator';
|
||||
import { IKbnUrlStateStorage } from '../../services/kibana_utils';
|
||||
|
||||
export const getDashboardListItemLink = (
|
||||
application: ApplicationStart,
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage,
|
||||
useHash: boolean,
|
||||
id: string,
|
||||
timeRestore: boolean
|
||||
) => {
|
||||
let url = application.getUrlForApp(DashboardConstants.DASHBOARDS_ID, {
|
||||
path: `#${createDashboardEditUrl(id)}`,
|
||||
});
|
||||
const globalStateInUrl = kbnUrlStateStorage.get<QueryState>(GLOBAL_STATE_STORAGE_KEY) || {};
|
||||
|
||||
if (timeRestore) {
|
||||
delete globalStateInUrl.time;
|
||||
delete globalStateInUrl.refreshInterval;
|
||||
}
|
||||
url = setStateToKbnUrl<QueryState>(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url);
|
||||
return url;
|
||||
};
|
|
@ -7,3 +7,5 @@
|
|||
*/
|
||||
|
||||
export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size';
|
||||
export const STATE_STORAGE_KEY = '_a';
|
||||
export const GLOBAL_STATE_STORAGE_KEY = '_g';
|
||||
|
|
|
@ -40,6 +40,7 @@ export const VisualizeListing = () => {
|
|||
savedObjectsTagging,
|
||||
uiSettings,
|
||||
visualizeCapabilities,
|
||||
kbnUrlStateStorage,
|
||||
},
|
||||
} = useKibana<VisualizeServices>();
|
||||
const { pathname } = useLocation();
|
||||
|
@ -94,11 +95,10 @@ export const VisualizeListing = () => {
|
|||
);
|
||||
|
||||
const noItemsFragment = useMemo(() => getNoItemsMessage(createNewVis), [createNewVis]);
|
||||
const tableColumns = useMemo(() => getTableColumns(application, history, savedObjectsTagging), [
|
||||
application,
|
||||
history,
|
||||
savedObjectsTagging,
|
||||
]);
|
||||
const tableColumns = useMemo(
|
||||
() => getTableColumns(application, kbnUrlStateStorage, savedObjectsTagging),
|
||||
[application, kbnUrlStateStorage, savedObjectsTagging]
|
||||
);
|
||||
|
||||
const fetchItems = useCallback(
|
||||
(filter) => {
|
||||
|
|
|
@ -7,14 +7,15 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { History } from 'history';
|
||||
import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink, EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public';
|
||||
import { VisualizationListItem } from 'src/plugins/visualizations/public';
|
||||
import type { SavedObjectsTaggingApi } from 'src/plugins/saved_objects_tagging_oss/public';
|
||||
import { RedirectAppLinks } from '../../../../kibana_react/public';
|
||||
import { getVisualizeListItemLink } from './get_visualize_list_item_link';
|
||||
|
||||
const getBadge = (item: VisualizationListItem) => {
|
||||
if (item.stage === 'beta') {
|
||||
|
@ -72,7 +73,7 @@ const renderItemTypeIcon = (item: VisualizationListItem) => {
|
|||
|
||||
export const getTableColumns = (
|
||||
application: ApplicationStart,
|
||||
history: History,
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage,
|
||||
taggingApi?: SavedObjectsTaggingApi
|
||||
) => [
|
||||
{
|
||||
|
@ -84,18 +85,14 @@ export const getTableColumns = (
|
|||
render: (field: string, { editApp, editUrl, title, error }: VisualizationListItem) =>
|
||||
// In case an error occurs i.e. the vis has wrong type, we render the vis but without the link
|
||||
!error ? (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
if (editApp) {
|
||||
application.navigateToApp(editApp, { path: editUrl });
|
||||
} else if (editUrl) {
|
||||
history.push(editUrl);
|
||||
}
|
||||
}}
|
||||
data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`}
|
||||
>
|
||||
{field}
|
||||
</EuiLink>
|
||||
<RedirectAppLinks application={application}>
|
||||
<EuiLink
|
||||
href={getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl)}
|
||||
data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`}
|
||||
>
|
||||
{field}
|
||||
</EuiLink>
|
||||
</RedirectAppLinks>
|
||||
) : (
|
||||
field
|
||||
),
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getVisualizeListItemLink } from './get_visualize_list_item_link';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { createHashHistory } from 'history';
|
||||
import { createKbnUrlStateStorage } from '../../../../kibana_utils/public';
|
||||
import { esFilters } from '../../../../data/public';
|
||||
import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants';
|
||||
|
||||
jest.mock('../../services', () => {
|
||||
return {
|
||||
getUISettings: () => ({
|
||||
get: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const application = ({
|
||||
getUrlForApp: jest.fn((appId: string, options?: { path?: string; absolute?: boolean }) => {
|
||||
return `/app/${appId}${options?.path}`;
|
||||
}),
|
||||
} as unknown) as ApplicationStart;
|
||||
|
||||
const history = createHashHistory();
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
});
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } });
|
||||
|
||||
describe('listing item link is correct for each app', () => {
|
||||
test('creates a link to classic visualization if editApp is not defined', async () => {
|
||||
const editUrl = 'edit/id';
|
||||
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, undefined, editUrl);
|
||||
expect(url).toMatchInlineSnapshot(`"/app/visualize#${editUrl}?_g=(time:(from:now-7d,to:now))"`);
|
||||
});
|
||||
|
||||
test('creates a link for the app given if editApp is defined', async () => {
|
||||
const editUrl = '#/edit/id';
|
||||
const editApp = 'lens';
|
||||
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
|
||||
expect(url).toMatchInlineSnapshot(`"/app/${editApp}${editUrl}?_g=(time:(from:now-7d,to:now))"`);
|
||||
});
|
||||
|
||||
describe('when global time changes', () => {
|
||||
beforeEach(() => {
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
|
||||
time: {
|
||||
from: '2021-01-05T11:45:53.375Z',
|
||||
to: '2021-01-21T11:46:00.990Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it propagates the correct time on the query', async () => {
|
||||
const editUrl = '#/edit/id';
|
||||
const editApp = 'lens';
|
||||
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/${editApp}${editUrl}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when global refreshInterval changes', () => {
|
||||
beforeEach(() => {
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
|
||||
refreshInterval: { pause: false, value: 300 },
|
||||
});
|
||||
});
|
||||
|
||||
test('it propagates the refreshInterval on the query', async () => {
|
||||
const editUrl = '#/edit/id';
|
||||
const editApp = 'lens';
|
||||
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/${editApp}${editUrl}?_g=(refreshInterval:(pause:!f,value:300))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when global filters change', () => {
|
||||
beforeEach(() => {
|
||||
const filters = [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
query: { query: 'q1' },
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
query: { query: 'q1' },
|
||||
$state: {
|
||||
store: esFilters.FilterStateStore.GLOBAL_STATE,
|
||||
},
|
||||
},
|
||||
];
|
||||
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
|
||||
filters,
|
||||
});
|
||||
});
|
||||
|
||||
test('propagates the filters on the query', async () => {
|
||||
const editUrl = '#/edit/id';
|
||||
const editApp = 'lens';
|
||||
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"/app/${editApp}${editUrl}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public';
|
||||
import { QueryState } from '../../../../data/public';
|
||||
import { setStateToKbnUrl } from '../../../../kibana_utils/public';
|
||||
import { getUISettings } from '../../services';
|
||||
import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants';
|
||||
import { APP_NAME } from '../visualize_constants';
|
||||
|
||||
export const getVisualizeListItemLink = (
|
||||
application: ApplicationStart,
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage,
|
||||
editApp: string | undefined,
|
||||
editUrl: string
|
||||
) => {
|
||||
// for visualizations the editApp is undefined
|
||||
let url = application.getUrlForApp(editApp ?? APP_NAME, {
|
||||
path: editApp ? editUrl : `#${editUrl}`,
|
||||
});
|
||||
const useHash = getUISettings().get('state:storeInSessionStorage');
|
||||
const globalStateInUrl = kbnUrlStateStorage.get<QueryState>(GLOBAL_STATE_STORAGE_KEY) || {};
|
||||
|
||||
url = setStateToKbnUrl<QueryState>(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url);
|
||||
return url;
|
||||
};
|
|
@ -16,9 +16,7 @@ import {
|
|||
} from '../../data/public';
|
||||
import { setStateToKbnUrl } from '../../kibana_utils/public';
|
||||
import { UrlGeneratorsDefinition } from '../../share/public';
|
||||
|
||||
const STATE_STORAGE_KEY = '_a';
|
||||
const GLOBAL_STATE_STORAGE_KEY = '_g';
|
||||
import { STATE_STORAGE_KEY, GLOBAL_STATE_STORAGE_KEY } from '../common/constants';
|
||||
|
||||
export const VISUALIZE_APP_URL_GENERATOR = 'VISUALIZE_APP_URL_GENERATOR';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue