Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SecuritySolution] Dashboard listing UI #160540

Merged
merged 41 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
716fd81
replace dashboard listing
angorayc Jun 26, 2023
071dfb7
styling
angorayc Apr 25, 2023
eb41945
add fixed tag
angorayc May 16, 2023
aac6c3c
disable actions
angorayc May 16, 2023
df89974
default selected tags
angorayc Jun 26, 2023
3fa5710
fix tag types
angorayc Jun 26, 2023
93237cd
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 26, 2023
a08e4ce
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 26, 2023
ce5e867
fix types
angorayc Jun 27, 2023
59a293d
unit tests
angorayc Jun 27, 2023
0413ea0
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 27, 2023
3429a79
consume TableListViewTable from dashboard listing
angorayc Jun 27, 2023
c51202c
revert tagReferences
angorayc Jun 27, 2023
94c0758
fix filter count
angorayc Jun 27, 2023
c86641c
rename
angorayc Jun 27, 2023
7e8ce70
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 28, 2023
8573325
styling
angorayc Jun 28, 2023
884643f
Merge branch 'dashboard-listing' of github.com:angorayc/kibana into d…
angorayc Jun 28, 2023
ffebde7
unit test
angorayc Jun 28, 2023
288e21e
unit tests
angorayc Jun 28, 2023
7b068b2
Merge branch 'main' of github.com:elastic/kibana into dashboard-listing
angorayc Jun 28, 2023
ab434ac
revert changes for fixing active tag count
angorayc Jun 28, 2023
875b3b9
add tests
angorayc Jun 28, 2023
a578b94
types
angorayc Jun 28, 2023
f1ffe31
Merge branch 'main' into dashboard-listing
angorayc Jun 28, 2023
521e978
revert change
angorayc Jun 30, 2023
d11ff02
Merge branch 'main' of github.com:elastic/kibana into dashboard-listing
angorayc Jun 30, 2023
0b5c270
export dashboardListingTable
angorayc Jun 30, 2023
060bca6
Merge branch 'main' of github.com:elastic/kibana into dashboard-listing
angorayc Jun 30, 2023
448ee22
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 30, 2023
20d1511
types
angorayc Jun 30, 2023
cbc4b7d
Merge branch 'dashboard-listing' of github.com:angorayc/kibana into d…
angorayc Jun 30, 2023
d6e2435
test
angorayc Jun 30, 2023
34ba502
Implement review sugestions
machadoum Jul 5, 2023
cad6053
Merge branch 'main' into dashboard-listing
angorayc Jul 18, 2023
59bd25a
review
angorayc Jul 18, 2023
2afacc4
review
angorayc Jul 18, 2023
93ee16a
Merge branch 'main' of github.com:elastic/kibana into dashboard-listing
angorayc Jul 24, 2023
df1000c
use savedObjectsTagging client
angorayc Jul 24, 2023
5b028b0
unit test
angorayc Jul 24, 2023
c0ffaea
review
angorayc Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { mount, ReactWrapper } from 'enzyme';
import { I18nProvider } from '@kbn/i18n-react';

import { pluginServices } from '../services/plugin_services';
import { DashboardListing, DashboardListingProps } from './dashboard_listing';
import { DashboardListing } from './dashboard_listing';

/**
* Mock Table List view. This dashboard component is a wrapper around the shared UX table List view. We
* need to ensure we're passing down the correct props, but the table list view itself doesn't need to be rendered
* in our tests because it is covered in its package.
*/
import { TableListView } from '@kbn/content-management-table-list-view';
import { DashboardListingProps } from './types';
// import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view';
jest.mock('@kbn/content-management-table-list-view-table', () => {
const originalModule = jest.requireActual('@kbn/content-management-table-list-view-table');
Expand Down
206 changes: 19 additions & 187 deletions src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,25 @@
*/

import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import React, { PropsWithChildren, useCallback, useMemo, useState } from 'react';
import React, { useMemo } from 'react';

import {
type TableListViewKibanaDependencies,
TableListViewKibanaProvider,
type UserContentCommonSchema,
} from '@kbn/content-management-table-list-view-table';
import { TableListView } from '@kbn/content-management-table-list-view';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type { SavedObjectsFindOptionsReference } from '@kbn/core/public';

import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public';

import {
DASHBOARD_CONTENT_ID,
SAVED_OBJECT_DELETE_TIME,
SAVED_OBJECT_LOADED_TIME,
} from '../dashboard_constants';
import {
dashboardListingTableStrings,
dashboardListingErrorStrings,
} from './_dashboard_listing_strings';
import { pluginServices } from '../services/plugin_services';
import { confirmCreateWithUnsaved } from './confirm_overlays';
import { DashboardItem } from '../../common/content_management';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { DashboardApplicationService } from '../services/application/types';
import { DashboardListingEmptyPrompt } from './dashboard_listing_empty_prompt';

// because the type of `application.capabilities.advancedSettings` is so generic, the provider
// requiring the `save` key to be part of it is causing type issues - so, creating a custom type
type TableListViewApplicationService = DashboardApplicationService & {
capabilities: { advancedSettings: { save: boolean } };
};

const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit';
const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage';

interface DashboardSavedObjectUserContent extends UserContentCommonSchema {
attributes: {
title: string;
description?: string;
timeRestore: boolean;
};
}

const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUserContent => {
const { title, description, timeRestore } = hit.attributes;
return {
type: 'dashboard',
id: hit.id,
updatedAt: hit.updatedAt!,
references: hit.references,
attributes: {
title,
description,
timeRestore,
},
};
};

export type DashboardListingProps = PropsWithChildren<{
initialFilter?: string;
useSessionStorageIntegration?: boolean;
goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void;
getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string;
}>;
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { useDashboardListingTable } from './hooks/use_dashboard_listing_table';
import {
DashboardListingProps,
DashboardSavedObjectUserContent,
TableListViewApplicationService,
} from './types';

export const DashboardListing = ({
children,
Expand All @@ -89,123 +41,22 @@ export const DashboardListing = ({
http,
chrome: { theme },
savedObjectsTagging,
dashboardSessionStorage,
settings: { uiSettings },
notifications: { toasts },

coreContext: { executionContext },
dashboardCapabilities: { showWriteControls },
dashboardContentManagement: { findDashboards, deleteDashboards },
} = pluginServices.getServices();

const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()
);

useExecutionContext(executionContext, {
type: 'application',
page: 'list',
});

const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING);
const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING);

const createItem = useCallback(() => {
if (useSessionStorageIntegration && dashboardSessionStorage.dashboardHasUnsavedEdits()) {
confirmCreateWithUnsaved(() => {
dashboardSessionStorage.clearState();
goToDashboard();
}, goToDashboard);
return;
}
goToDashboard();
}, [dashboardSessionStorage, goToDashboard, useSessionStorageIntegration]);

const fetchItems = useCallback(
(
searchTerm: string,
{
references,
referencesToExclude,
}: {
references?: SavedObjectsFindOptionsReference[];
referencesToExclude?: SavedObjectsFindOptionsReference[];
} = {}
) => {
const searchStartTime = window.performance.now();

return findDashboards
.search({
search: searchTerm,
size: listingLimit,
hasReference: references,
hasNoReference: referencesToExclude,
})
.then(({ total, hits }) => {
const searchEndTime = window.performance.now();
const searchDuration = searchEndTime - searchStartTime;
reportPerformanceMetricEvent(pluginServices.getServices().analytics, {
eventName: SAVED_OBJECT_LOADED_TIME,
duration: searchDuration,
meta: {
saved_object_type: DASHBOARD_CONTENT_ID,
},
});
return {
total,
hits: hits.map(toTableListViewSavedObject),
};
});
},
[findDashboards, listingLimit]
);

const deleteItems = useCallback(
async (dashboardsToDelete: Array<{ id: string }>) => {
try {
const deleteStartTime = window.performance.now();

await deleteDashboards(
dashboardsToDelete.map(({ id }) => {
dashboardSessionStorage.clearState(id);
return id;
})
);

const deleteDuration = window.performance.now() - deleteStartTime;
reportPerformanceMetricEvent(pluginServices.getServices().analytics, {
eventName: SAVED_OBJECT_DELETE_TIME,
duration: deleteDuration,
meta: {
saved_object_type: DASHBOARD_CONTENT_ID,
total: dashboardsToDelete.length,
},
});
} catch (error) {
toasts.addError(error, {
title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(),
});
}

setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
},
[dashboardSessionStorage, deleteDashboards, toasts]
);

const editItem = useCallback(
({ id }: { id: string | undefined }) => goToDashboard(id, ViewMode.EDIT),
[goToDashboard]
);
const emptyPrompt = (
<DashboardListingEmptyPrompt
createItem={createItem}
goToDashboard={goToDashboard}
unsavedDashboardIds={unsavedDashboardIds}
setUnsavedDashboardIds={setUnsavedDashboardIds}
useSessionStorageIntegration={useSessionStorageIntegration}
/>
);

const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTableStrings;
const { unsavedDashboardIds, refreshUnsavedDashboards, tableListViewTableProps } =
useDashboardListingTable({
goToDashboard,
getDashboardUrl,
useSessionStorageIntegration,
initialFilter,
});

const savedObjectsTaggingFakePlugin = useMemo(() => {
return savedObjectsTagging.hasApi // TODO: clean up this logic once https://github.com/elastic/kibana/issues/140433 is resolved
Expand All @@ -231,32 +82,13 @@ export const DashboardListing = ({
FormattedRelative,
}}
>
<TableListView<DashboardSavedObjectUserContent>
getDetailViewLink={({ id, attributes: { timeRestore } }) =>
getDashboardUrl(id, timeRestore)
}
deleteItems={!showWriteControls ? undefined : deleteItems}
createItem={!showWriteControls ? undefined : createItem}
editItem={!showWriteControls ? undefined : editItem}
entityNamePlural={getEntityNamePlural()}
title={getTableListTitle()}
headingId="dashboardListingHeading"
initialPageSize={initialPageSize}
initialFilter={initialFilter}
entityName={getEntityName()}
listingLimit={listingLimit}
emptyPrompt={emptyPrompt}
findItems={fetchItems}
id="dashboard"
>
<TableListView<DashboardSavedObjectUserContent> {...tableListViewTableProps}>
<>
{children}
<DashboardUnsavedListing
goToDashboard={goToDashboard}
unsavedDashboardIds={unsavedDashboardIds}
refreshUnsavedDashboards={() =>
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges())
}
refreshUnsavedDashboards={refreshUnsavedDashboards}
/>
</>
</TableListView>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const makeDefaultProps = (): DashboardListingEmptyPromptProps => ({
goToDashboard: jest.fn(),
setUnsavedDashboardIds: jest.fn(),
useSessionStorageIntegration: true,
disableCreateDashboardButton: false,
});

function mountWith({
Expand Down Expand Up @@ -75,6 +76,21 @@ test('renders empty prompt with link when showWriteControls is on', async () =>
expect(component!.find('EuiLink').length).toBe(1);
});

test('renders disabled action button when disableCreateDashboardButton is true', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;

let component: ReactWrapper;
await act(async () => {
({ component } = mountWith({ props: { disableCreateDashboardButton: true } }));
});

component!.update();

expect(component!.find(`[data-test-subj="newItemButton"]`).first().prop('disabled')).toEqual(
true
);
});

test('renders continue button when no dashboards exist but one is in progress', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;
let component: ReactWrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import {
getNewDashboardTitle,
dashboardUnsavedListingStrings,
} from './_dashboard_listing_strings';
import { DashboardListingProps } from './dashboard_listing';
import { pluginServices } from '../services/plugin_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service';
import { DashboardListingProps } from './types';

export interface DashboardListingEmptyPromptProps {
createItem: () => void;
disableCreateDashboardButton?: boolean;
unsavedDashboardIds: string[];
goToDashboard: DashboardListingProps['goToDashboard'];
setUnsavedDashboardIds: React.Dispatch<React.SetStateAction<string[]>>;
Expand All @@ -41,6 +42,7 @@ export const DashboardListingEmptyPrompt = ({
unsavedDashboardIds,
goToDashboard,
createItem,
disableCreateDashboardButton,
}: DashboardListingEmptyPromptProps) => {
const {
application,
Expand All @@ -56,7 +58,13 @@ export const DashboardListingEmptyPrompt = ({
const getEmptyAction = useCallback(() => {
if (!isEditingFirstDashboard) {
return (
<EuiButton onClick={createItem} fill iconType="plusInCircle" data-test-subj="newItemButton">
<EuiButton
onClick={createItem}
fill
iconType="plusInCircle"
data-test-subj="newItemButton"
disabled={disableCreateDashboardButton}
>
{noItemsStrings.getCreateNewDashboardText()}
</EuiButton>
);
Expand Down Expand Up @@ -94,11 +102,12 @@ export const DashboardListingEmptyPrompt = ({
</EuiFlexGroup>
);
}, [
dashboardSessionStorage,
isEditingFirstDashboard,
createItem,
disableCreateDashboardButton,
dashboardSessionStorage,
setUnsavedDashboardIds,
goToDashboard,
createItem,
]);

if (!showWriteControls) {
Expand Down
Loading