mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Eui dashboard listing (#16967)
* convert dashboard listing page to react and EUI * add jest test for DashboardListing component * add data-test-subj attributes * clean up jest test * hideWriteControls and call to action when no dashboards exist * pass initial filter to dashboard listing, get functional tests to work * fix dashboard queries functional tests * upgraded to EUI 0.0.29 to get defaultFocusedButton fix * move dashboardListing directive to index * spacing in if statement * switch to EuiBasicTable * pagination * add sorting * fix jest test * handle out of order fetchs * remove info.gif * re-instate search functional test * replace EuiSearchBar with EuiFieldSearch * fix functional tests * update snapshot - when code rebased - new EUI version add another prop * add Edit link to actions column
This commit is contained in:
parent
b95b3f4fc1
commit
bed97a27b0
9 changed files with 1356 additions and 440 deletions
|
@ -6,14 +6,25 @@ import uiRoutes from 'ui/routes';
|
|||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import dashboardTemplate from './dashboard_app.html';
|
||||
import dashboardListingTemplate from './listing/dashboard_listing.html';
|
||||
import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html';
|
||||
|
||||
import { DashboardListingController } from './listing/dashboard_listing';
|
||||
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
|
||||
import { SavedObjectNotFound } from 'ui/errors';
|
||||
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { recentlyAccessed } from 'ui/persisted_log';
|
||||
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
|
||||
import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing';
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('app/dashboard', [
|
||||
'ngRoute',
|
||||
'react',
|
||||
]);
|
||||
|
||||
app.directive('dashboardListing', function (reactDirective) {
|
||||
return reactDirective(DashboardListing);
|
||||
});
|
||||
|
||||
uiRoutes
|
||||
.defaults(/dashboard/, {
|
||||
|
@ -21,8 +32,20 @@ uiRoutes
|
|||
})
|
||||
.when(DashboardConstants.LANDING_PAGE_PATH, {
|
||||
template: dashboardListingTemplate,
|
||||
controller: DashboardListingController,
|
||||
controllerAs: 'listingController',
|
||||
controller($injector, $location, $scope, Private, config) {
|
||||
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
|
||||
const dashboardConfig = $injector.get('dashboardConfig');
|
||||
|
||||
$scope.listingLimit = config.get('savedObjects:listingLimit');
|
||||
$scope.find = (search) => {
|
||||
return services.dashboards.find(search, $scope.listingLimit);
|
||||
};
|
||||
$scope.delete = (ids) => {
|
||||
return services.dashboards.delete(ids);
|
||||
};
|
||||
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
|
||||
$scope.initialFilter = ($location.search()).filter || EMPTY_FILTER;
|
||||
},
|
||||
resolve: {
|
||||
dash: function ($route, Private, courier, kbnUrl) {
|
||||
const savedObjectsClient = Private(SavedObjectsClientProvider);
|
||||
|
|
|
@ -0,0 +1,788 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`after fetch hideWriteControls 1`] = `
|
||||
<EuiPage
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
component="div"
|
||||
data-test-subj="top-nav"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="searchFilter"
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Search..."
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemIdToExpandedRowMap={Object {}}
|
||||
items={Array []}
|
||||
loading={false}
|
||||
noItemsMessage={
|
||||
<EuiText>
|
||||
<h2>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
Looks like you don't have any dashboards.
|
||||
</EuiTextColor>
|
||||
</h2>
|
||||
</EuiText>
|
||||
}
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={Object {}}
|
||||
/>
|
||||
</EuiPage>
|
||||
`;
|
||||
|
||||
exports[`after fetch renders call to action when no dashboards exist 1`] = `
|
||||
<EuiPage
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
component="div"
|
||||
data-test-subj="top-nav"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="newDashboardLink"
|
||||
fill={false}
|
||||
href="#/dashboard"
|
||||
iconSide="left"
|
||||
type="button"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="searchFilter"
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Search..."
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
itemIdToExpandedRowMap={Object {}}
|
||||
items={Array []}
|
||||
loading={false}
|
||||
noItemsMessage={
|
||||
<UNDEFINED>
|
||||
<EuiText>
|
||||
<h2>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
Looks like you don't have any dashboards. Let's create some!
|
||||
</EuiTextColor>
|
||||
</h2>
|
||||
</EuiText>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
fill={true}
|
||||
href="#/dashboard"
|
||||
iconSide="left"
|
||||
iconType="plusInCircle"
|
||||
type="button"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</UNDEFINED>
|
||||
}
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={Object {}}
|
||||
/>
|
||||
</EuiPage>
|
||||
`;
|
||||
|
||||
exports[`after fetch renders table rows 1`] = `
|
||||
<EuiPage
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
component="div"
|
||||
data-test-subj="top-nav"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="newDashboardLink"
|
||||
fill={false}
|
||||
href="#/dashboard"
|
||||
iconSide="left"
|
||||
type="button"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="searchFilter"
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Search..."
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
itemIdToExpandedRowMap={Object {}}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"description": "dashboard0 desc",
|
||||
"id": "dashboard0",
|
||||
"title": "dashboard0 title",
|
||||
},
|
||||
Object {
|
||||
"description": "dashboard1 desc",
|
||||
"id": "dashboard1",
|
||||
"title": "dashboard1 title",
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={false}
|
||||
noItemsMessage="No dashboards matched your search."
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 2,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={Object {}}
|
||||
/>
|
||||
</EuiPage>
|
||||
`;
|
||||
|
||||
exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
|
||||
<EuiPage
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
component="div"
|
||||
data-test-subj="top-nav"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="newDashboardLink"
|
||||
fill={false}
|
||||
href="#/dashboard"
|
||||
iconSide="left"
|
||||
type="button"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<React.Fragment>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
size="m"
|
||||
title="Listing limit exceeded"
|
||||
>
|
||||
<p>
|
||||
You have
|
||||
2
|
||||
dashboards, but your
|
||||
<strong>
|
||||
listingLimit
|
||||
</strong>
|
||||
setting prevents the table below from displaying more than
|
||||
1
|
||||
. You can change this setting under
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="#/management/kibana/settings"
|
||||
type="button"
|
||||
>
|
||||
Advanced Settings
|
||||
</EuiLink>
|
||||
.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
</React.Fragment>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="searchFilter"
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Search..."
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
itemIdToExpandedRowMap={Object {}}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"description": "dashboard0 desc",
|
||||
"id": "dashboard0",
|
||||
"title": "dashboard0 title",
|
||||
},
|
||||
Object {
|
||||
"description": "dashboard1 desc",
|
||||
"id": "dashboard1",
|
||||
"title": "dashboard1 title",
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={false}
|
||||
noItemsMessage="No dashboards matched your search."
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 2,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={Object {}}
|
||||
/>
|
||||
</EuiPage>
|
||||
`;
|
||||
|
||||
exports[`initialFilter 1`] = `
|
||||
<EuiPage
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
component="div"
|
||||
data-test-subj="top-nav"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="newDashboardLink"
|
||||
fill={false}
|
||||
href="#/dashboard"
|
||||
iconSide="left"
|
||||
type="button"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="searchFilter"
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Search..."
|
||||
value="my dashboard"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
itemIdToExpandedRowMap={Object {}}
|
||||
items={Array []}
|
||||
loading={true}
|
||||
noItemsMessage=""
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={Object {}}
|
||||
/>
|
||||
</EuiPage>
|
||||
`;
|
||||
|
||||
exports[`renders table in loading state 1`] = `
|
||||
<EuiPage
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
component="div"
|
||||
data-test-subj="top-nav"
|
||||
gutterSize="l"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="newDashboardLink"
|
||||
fill={false}
|
||||
href="#/dashboard"
|
||||
iconSide="left"
|
||||
type="button"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="searchFilter"
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
placeholder="Search..."
|
||||
value=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
itemIdToExpandedRowMap={Object {}}
|
||||
items={Array []}
|
||||
loading={true}
|
||||
noItemsMessage=""
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 20,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
sorting={Object {}}
|
||||
/>
|
||||
</EuiPage>
|
||||
`;
|
|
@ -1,268 +0,0 @@
|
|||
<!-- Local nav. -->
|
||||
<kbn-top-nav name="dashboard">
|
||||
<!-- Transcluded elements. -->
|
||||
<div data-transclude-slots>
|
||||
<!-- Title. -->
|
||||
<div
|
||||
data-transclude-slot="topLeftCorner"
|
||||
class="kuiLocalTitle"
|
||||
role="heading"
|
||||
aria-level="1"
|
||||
>
|
||||
Dashboard
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
<div
|
||||
class="kuiViewContent kuiViewContent--constrainedWidth"
|
||||
data-test-subj="dashboardLandingPage"
|
||||
>
|
||||
<div class="kuiViewContentItem kuiVerticalRhythm" ng-if="listingController.showLimitError">
|
||||
<div class="kuiInfoPanel kuiInfoPanel--warning">
|
||||
<div class="kuiInfoPanelBody">
|
||||
<div class="kuiInfoPanelBody__message">
|
||||
You have {{ listingController.totalItems }} dashboards, but your "listingLimit" setting prevents the table below from displaying more than {{ listingController.listingLimit }}. You can change this setting under <a kbn-href="#/management/kibana/settings" class="kuiLink">Advanced Settings</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ControlledTable -->
|
||||
<div class="kuiViewContentItem kuiControlledTable kuiVerticalRhythm">
|
||||
<!-- ToolBar -->
|
||||
<div class="kuiToolBar">
|
||||
<div class="kuiToolBarSearch">
|
||||
<div class="kuiToolBarSearchBox">
|
||||
<div class="kuiToolBarSearchBox__icon kuiIcon fa-search"></div>
|
||||
<input
|
||||
class="kuiToolBarSearchBox__input"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
aria-label="Filter dashboards"
|
||||
data-test-subj="searchFilter"
|
||||
ng-model="listingController.filter"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiToolBarSection">
|
||||
<!-- Bulk delete button -->
|
||||
<button
|
||||
class="kuiButton kuiButton--danger"
|
||||
ng-click="listingController.deleteSelectedItems()"
|
||||
aria-label="Delete selected dashboards"
|
||||
ng-if="listingController.getSelectedItemsCount() > 0 && !listingController.hideWriteControls"
|
||||
tooltip="Delete selected dashboards"
|
||||
tooltip-append-to-body="true"
|
||||
data-test-subj="deleteSelectedDashboards"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-trash"></span>
|
||||
</button>
|
||||
|
||||
<!-- Create dashboard button -->
|
||||
<a
|
||||
class="kuiButton kuiButton--primary"
|
||||
href="{{listingController.getCreateDashboardHref()}}"
|
||||
aria-label="Create new dashboard"
|
||||
data-test-subj="newDashboardLink"
|
||||
ng-if="listingController.getSelectedItemsCount() === 0 && !listingController.hideWriteControls"
|
||||
tooltip="Create new dashboard"
|
||||
tooltip-append-to-body="true"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="kuiToolBarSection">
|
||||
<!-- Pagination -->
|
||||
<tool-bar-pager-text
|
||||
start-item="listingController.pager.startItem"
|
||||
end-item="listingController.pager.endItem"
|
||||
total-items="listingController.pager.totalItems"
|
||||
></tool-bar-pager-text>
|
||||
<tool-bar-pager-buttons
|
||||
has-previous-page="listingController.pager.hasPreviousPage"
|
||||
has-next-page="listingController.pager.hasNextPage"
|
||||
on-page-next="listingController.onPageNext"
|
||||
on-page-previous="listingController.onPagePrevious"
|
||||
></tool-bar-pager-buttons>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TableInfo -->
|
||||
<div
|
||||
class="kuiPanel kuiPanel--centered kuiPanel--withToolBar"
|
||||
ng-if="!listingController.items.length && listingController.filter"
|
||||
>
|
||||
<div class="kuiTableInfo">
|
||||
No dashboards matched your search.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EmptyTablePrompt -->
|
||||
<div
|
||||
class="kuiPanel kuiPanel--centered kuiPanel--withToolBar"
|
||||
ng-if="!listingController.isFetchingItems && !listingController.items.length && !listingController.filter"
|
||||
>
|
||||
<div class="kuiEmptyTablePrompt">
|
||||
<div class="kuiEmptyTablePrompt__message">
|
||||
Looks like you don’t have any dashboards. <span ng-if="!listingController.hideWriteControls">Let’s create some!</span>
|
||||
</div>
|
||||
|
||||
<div class="kuiEmptyTablePrompt__actions" ng-if="!listingController.hideWriteControls">
|
||||
<a
|
||||
class="kuiButton kuiButton--primary kuiButton--iconText"
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
href="{{listingController.getCreateDashboardHref()}}"
|
||||
>
|
||||
<span class="kuiButton__inner">
|
||||
<span class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
<span>Create a dashboard</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<table class="kuiTable dashboardListingTable" ng-if="listingController.items.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="kuiTableHeaderCell kuiTableHeaderCell--checkBox"
|
||||
ng-if="!listingController.hideWriteControls"
|
||||
scope="col"
|
||||
>
|
||||
<div class="kuiTableHeaderCell__liner">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="kuiCheckBox"
|
||||
ng-checked="listingController.areAllItemsChecked()"
|
||||
ng-click="listingController.toggleAll()"
|
||||
aria-label="{{listingController.areAllItemsChecked() ? 'Deselect all rows' : 'Select all rows'}}"
|
||||
>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="kuiTableHeaderCell">
|
||||
<button
|
||||
class="kuiTableHeaderCellButton"
|
||||
ng-class="{'kuiTableHeaderCellButton-isSorted': listingController.getSortedProperty().name == 'title'}"
|
||||
ng-click="listingController.sortOn('title')"
|
||||
aria-label="{{listingController.isAscending('title') ? 'Sort name descending' : 'Sort name ascending'}}"
|
||||
>
|
||||
<span class="kuiTableHeaderCell__liner">
|
||||
Name
|
||||
<span
|
||||
class="kuiTableSortIcon kuiIcon"
|
||||
ng-class="listingController.isAscending('title') ? 'fa-long-arrow-up' : 'fa-long-arrow-down'"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="kuiTableHeaderCell">
|
||||
<button
|
||||
class="kuiTableHeaderCellButton"
|
||||
ng-class="{'kuiTableHeaderCellButton-isSorted': listingController.getSortedProperty().name == 'description'}"
|
||||
ng-click="listingController.sortOn('description')"
|
||||
aria-label="{{listingController.isAscending('description') ? 'Sort description descending' : 'Sort description ascending'}}"
|
||||
>
|
||||
<span class="kuiTableHeaderCell__liner">
|
||||
Description
|
||||
<span
|
||||
class="kuiTableSortIcon kuiIcon"
|
||||
ng-class="listingController.isAscending('description') ? 'fa-long-arrow-up' : 'fa-long-arrow-down'"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
</th>
|
||||
|
||||
<th
|
||||
ng-if="!listingController.hideWriteControls"
|
||||
scope="col"
|
||||
class="kuiTableHeaderCell actionBtnHeaderCell"
|
||||
>
|
||||
<div class="kuiTableHeaderCell__liner">Actions</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
ng-repeat="item in listingController.pageOfItems track by item.id"
|
||||
class="kuiTableRow"
|
||||
data-test-subj="dashboardListingRow"
|
||||
>
|
||||
<td class="kuiTableRowCell kuiTableRowCell--checkBox" ng-if="!listingController.hideWriteControls">
|
||||
<div class="kuiTableRowCell__liner">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="kuiCheckBox"
|
||||
data-test-subj="dashboardListItemCheckbox"
|
||||
ng-click="listingController.toggleItem(item)"
|
||||
ng-checked="listingController.isItemChecked(item)"
|
||||
aria-label="{{listingController.isItemChecked(item) ? 'Deselect row' : 'Select row'}}"
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="kuiTableRowCell">
|
||||
<div class="kuiTableRowCell__liner">
|
||||
<a
|
||||
class="kuiLink"
|
||||
data-test-subj="dashboardListingTitleLink-{{item.title.split(' ').join('-')}}"
|
||||
ng-href="{{ listingController.getUrlForItem(item) }}"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="kuiTableRowCell kuiTableRowCell--wrap">
|
||||
<div class="kuiTableRowCell__liner">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
ng-if="!listingController.hideWriteControls"
|
||||
class="kuiTableRowCell kuiTableRowCell--wrap"
|
||||
>
|
||||
<div class="kuiTableRowCell__liner">
|
||||
<a
|
||||
class="kuiMenuButton kuiMenuButton--basic"
|
||||
data-test-subj="dashboardListingTitleEditLink-{{item.title.split(' ').join('-')}}"
|
||||
ng-href="{{ listingController.getEditUrlForItem(item) }}"
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ToolBarFooter -->
|
||||
<div class="kuiToolBarFooter">
|
||||
<div class="kuiToolBarFooterSection">
|
||||
<div class="kuiToolBarText" ng-hide="listingController.getSelectedItemsCount() === 0">
|
||||
{{ listingController.getSelectedItemsCount() }} selected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiToolBarSection">
|
||||
<!-- Pagination -->
|
||||
<tool-bar-pager-text
|
||||
start-item="listingController.pager.startItem"
|
||||
end-item="listingController.pager.endItem"
|
||||
total-items="listingController.pager.totalItems"
|
||||
></tool-bar-pager-text>
|
||||
<tool-bar-pager-buttons
|
||||
has-previous-page="listingController.pager.hasPreviousPage"
|
||||
has-next-page="listingController.pager.hasNextPage"
|
||||
on-page-next="listingController.onPageNext"
|
||||
on-page-previous="listingController.onPagePrevious"
|
||||
></tool-bar-pager-buttons>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,168 +1,410 @@
|
|||
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
|
||||
import 'ui/pager_control';
|
||||
import 'ui/pager';
|
||||
import './dashboard_listing.less';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiFieldSearch,
|
||||
EuiBasicTable,
|
||||
EuiPage,
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiOverlayMask,
|
||||
EuiConfirmModal,
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants';
|
||||
import { SortableProperties } from '@elastic/eui';
|
||||
import { ConfirmationButtonTypes } from 'ui/modals';
|
||||
|
||||
export function DashboardListingController($injector, $scope, $location) {
|
||||
const $filter = $injector.get('$filter');
|
||||
const confirmModal = $injector.get('confirmModal');
|
||||
const Notifier = $injector.get('Notifier');
|
||||
const pagerFactory = $injector.get('pagerFactory');
|
||||
const Private = $injector.get('Private');
|
||||
const timefilter = $injector.get('timefilter');
|
||||
const config = $injector.get('config');
|
||||
const dashboardConfig = $injector.get('dashboardConfig');
|
||||
export const EMPTY_FILTER = '';
|
||||
|
||||
timefilter.disableAutoRefreshSelector();
|
||||
timefilter.disableTimeRangeSelector();
|
||||
// saved object client does not support sorting by title because title is only mapped as analyzed
|
||||
// the legacy implementation got around this by pulling `listingLimit` items and doing client side sorting
|
||||
// and not supporting server-side paging.
|
||||
// This component does not try to tackle these problems (yet) and is just feature matching the legacy component
|
||||
// TODO support server side sorting/paging once title and description are sortable on the server.
|
||||
export class DashboardListing extends React.Component {
|
||||
|
||||
const limitTo = $filter('limitTo');
|
||||
// TODO: Extract this into an external service.
|
||||
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
|
||||
const dashboardService = services.dashboards;
|
||||
const notify = new Notifier({ location: 'Dashboard' });
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let selectedItems = [];
|
||||
const sortableProperties = new SortableProperties([
|
||||
{
|
||||
name: 'title',
|
||||
getValue: item => item.title.toLowerCase(),
|
||||
isAscending: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
getValue: item => item.description.toLowerCase(),
|
||||
isAscending: true
|
||||
}
|
||||
],
|
||||
'title');
|
||||
|
||||
const calculateItemsOnPage = () => {
|
||||
this.items = sortableProperties.sortItems(this.items);
|
||||
this.pager.setTotalItems(this.items.length);
|
||||
this.pageOfItems = limitTo(this.items, this.pager.pageSize, this.pager.startIndex);
|
||||
};
|
||||
|
||||
const fetchItems = () => {
|
||||
this.isFetchingItems = true;
|
||||
|
||||
dashboardService.find(this.filter, config.get('savedObjects:listingLimit'))
|
||||
.then(result => {
|
||||
this.isFetchingItems = false;
|
||||
this.items = result.hits;
|
||||
this.totalItems = result.total;
|
||||
this.showLimitError = result.total > config.get('savedObjects:listingLimit');
|
||||
this.listingLimit = config.get('savedObjects:listingLimit');
|
||||
calculateItemsOnPage();
|
||||
});
|
||||
};
|
||||
|
||||
const deselectAll = () => {
|
||||
selectedItems = [];
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
selectedItems = this.pageOfItems.slice(0);
|
||||
};
|
||||
|
||||
this.isFetchingItems = false;
|
||||
this.items = [];
|
||||
this.pageOfItems = [];
|
||||
this.filter = ($location.search()).filter || '';
|
||||
|
||||
this.pager = pagerFactory.create(this.items.length, 20, 1);
|
||||
|
||||
this.hideWriteControls = dashboardConfig.getHideWriteControls();
|
||||
|
||||
$scope.$watch(() => this.filter, () => {
|
||||
deselectAll();
|
||||
fetchItems();
|
||||
$location.search('filter', this.filter);
|
||||
});
|
||||
this.isAscending = (name) => sortableProperties.isAscendingByName(name);
|
||||
this.getSortedProperty = () => sortableProperties.getSortedProperty();
|
||||
|
||||
this.sortOn = function sortOn(propertyName) {
|
||||
sortableProperties.sortOn(propertyName);
|
||||
deselectAll();
|
||||
calculateItemsOnPage();
|
||||
};
|
||||
|
||||
this.toggleAll = function toggleAll() {
|
||||
if (this.areAllItemsChecked()) {
|
||||
deselectAll();
|
||||
} else {
|
||||
selectAll();
|
||||
}
|
||||
};
|
||||
|
||||
this.toggleItem = function toggleItem(item) {
|
||||
if (this.isItemChecked(item)) {
|
||||
const index = selectedItems.indexOf(item);
|
||||
selectedItems.splice(index, 1);
|
||||
} else {
|
||||
selectedItems.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
this.isItemChecked = function isItemChecked(item) {
|
||||
return selectedItems.indexOf(item) !== -1;
|
||||
};
|
||||
|
||||
this.areAllItemsChecked = function areAllItemsChecked() {
|
||||
return this.getSelectedItemsCount() === this.pageOfItems.length;
|
||||
};
|
||||
|
||||
this.getSelectedItemsCount = function getSelectedItemsCount() {
|
||||
return selectedItems.length;
|
||||
};
|
||||
|
||||
this.deleteSelectedItems = function deleteSelectedItems() {
|
||||
const doDelete = () => {
|
||||
const selectedIds = selectedItems.map(item => item.id);
|
||||
|
||||
dashboardService.delete(selectedIds)
|
||||
.then(fetchItems)
|
||||
.then(() => {
|
||||
deselectAll();
|
||||
})
|
||||
.catch(error => notify.error(error));
|
||||
this.state = {
|
||||
isFetchingItems: false,
|
||||
showDeleteModal: false,
|
||||
showLimitError: false,
|
||||
filter: this.props.initialFilter,
|
||||
dashboards: [],
|
||||
selectedIds: [],
|
||||
page: 0,
|
||||
perPage: 20,
|
||||
};
|
||||
}
|
||||
|
||||
confirmModal(
|
||||
`You can't recover deleted dashboards.`,
|
||||
{
|
||||
confirmButtonText: 'Delete',
|
||||
onConfirm: doDelete,
|
||||
defaultFocusedButton: ConfirmationButtonTypes.CANCEL,
|
||||
title: 'Delete selected dashboards?'
|
||||
componentWillMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this.debouncedFetch.cancel();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchItems();
|
||||
}
|
||||
|
||||
debouncedFetch = _.debounce(async (filter) => {
|
||||
const response = await this.props.find(filter);
|
||||
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need this check to handle the case where search results come back in a different
|
||||
// order than they were sent out. Only load results for the most recent search.
|
||||
if (filter === this.state.filter) {
|
||||
this.setState({
|
||||
isFetchingItems: false,
|
||||
dashboards: response.hits,
|
||||
totalDashboards: response.total,
|
||||
showLimitError: response.total > this.props.listingLimit,
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
fetchItems = () => {
|
||||
this.setState({
|
||||
isFetchingItems: true,
|
||||
}, this.debouncedFetch.bind(null, this.state.filter));
|
||||
}
|
||||
|
||||
deleteSelectedItems = async () => {
|
||||
try {
|
||||
await this.props.delete(this.state.selectedIds);
|
||||
} catch (error) {
|
||||
toastNotifications.addDanger({
|
||||
title: `Unable to delete dashboard(s)`,
|
||||
text: `${error}`,
|
||||
});
|
||||
}
|
||||
this.fetchItems();
|
||||
this.setState({
|
||||
selectedIds: []
|
||||
});
|
||||
this.closeDeleteModal();
|
||||
}
|
||||
|
||||
closeDeleteModal = () => {
|
||||
this.setState({ showDeleteModal: false });
|
||||
};
|
||||
|
||||
this.onPageNext = () => {
|
||||
deselectAll();
|
||||
this.pager.nextPage();
|
||||
calculateItemsOnPage();
|
||||
openDeleteModal = () => {
|
||||
this.setState({ showDeleteModal: true });
|
||||
};
|
||||
|
||||
this.onPagePrevious = () => {
|
||||
deselectAll();
|
||||
this.pager.previousPage();
|
||||
calculateItemsOnPage();
|
||||
};
|
||||
onTableChange = ({ page, sort = {} }) => {
|
||||
const {
|
||||
index: pageIndex,
|
||||
size: pageSize,
|
||||
} = page;
|
||||
|
||||
this.getUrlForItem = function getUrlForItem(item) {
|
||||
return `#${createDashboardEditUrl(item.id)}`;
|
||||
};
|
||||
let {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
} = sort;
|
||||
|
||||
this.getEditUrlForItem = function getEditUrlForItem(item) {
|
||||
return `#${createDashboardEditUrl(item.id)}?_a=(viewMode:edit)`;
|
||||
};
|
||||
// 3rd sorting state that is not captured by sort - native order (no sort)
|
||||
// when switching from desc to asc for the same field - use native order
|
||||
if (this.state.sortField === sortField
|
||||
&& this.state.sortDirection === 'desc'
|
||||
&& sortDirection === 'asc') {
|
||||
sortField = null;
|
||||
sortDirection = null;
|
||||
}
|
||||
|
||||
this.getCreateDashboardHref = function getCreateDashboardHref() {
|
||||
return `#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`;
|
||||
};
|
||||
this.setState({
|
||||
page: pageIndex,
|
||||
perPage: pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
});
|
||||
}
|
||||
|
||||
// server-side paging not supported - see component comment for details
|
||||
getPageOfItems = () => {
|
||||
// do not sort original list to preserve elasticsearch ranking order
|
||||
const dashboardsCopy = this.state.dashboards.slice();
|
||||
|
||||
if (this.state.sortField) {
|
||||
dashboardsCopy.sort((a, b) => {
|
||||
const fieldA = _.get(a, this.state.sortField, '');
|
||||
const fieldB = _.get(b, this.state.sortField, '');
|
||||
let order = 1;
|
||||
if (this.state.sortDirection === 'desc') {
|
||||
order = -1;
|
||||
}
|
||||
return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// If begin is greater than the length of the sequence, an empty array is returned.
|
||||
const startIndex = this.state.page * this.state.perPage;
|
||||
// If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length).
|
||||
const lastIndex = startIndex + this.state.perPage;
|
||||
return dashboardsCopy.slice(startIndex, lastIndex);
|
||||
}
|
||||
|
||||
renderConfirmDeleteModal() {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title="Delete selected dashboards?"
|
||||
onCancel={this.closeDeleteModal}
|
||||
onConfirm={this.deleteSelectedItems}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
defaultFocusedButton="cancel"
|
||||
>
|
||||
<p>{`You can't recover deleted dashboards.`}</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
renderListingLimitWarning() {
|
||||
if (this.state.showLimitError) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiCallOut
|
||||
title="Listing limit exceeded"
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>
|
||||
You have {this.state.totalDashboards} dashboards,
|
||||
but your <strong>listingLimit</strong> setting prevents the table below from displaying more than {this.props.listingLimit}.
|
||||
You can change this setting under <EuiLink href="#/management/kibana/settings">Advanced Settings</EuiLink>.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderNoItemsMessage() {
|
||||
if (this.state.isFetchingItems) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!this.state.isFetchingItems && this.state.dashboards.length === 0 && !this.state.filter) {
|
||||
if (this.props.hideWriteControls) {
|
||||
return (
|
||||
<EuiText>
|
||||
<h2>
|
||||
<EuiTextColor color="subdued">
|
||||
{`Looks like you don't have any dashboards.`}
|
||||
</EuiTextColor>
|
||||
</h2>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiText>
|
||||
<h2>
|
||||
<EuiTextColor color="subdued">
|
||||
{`Looks like you don't have any dashboards. Let's create some!`}
|
||||
</EuiTextColor>
|
||||
</h2>
|
||||
</EuiText>
|
||||
<EuiButton
|
||||
href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return 'No dashboards matched your search.';
|
||||
}
|
||||
|
||||
renderSearchBar() {
|
||||
let deleteBtn;
|
||||
if (this.state.selectedIds.length > 0) {
|
||||
deleteBtn = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={this.openDeleteModal}
|
||||
data-test-subj="deleteSelectedDashboards"
|
||||
key="delete"
|
||||
>
|
||||
Delete selected
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
{deleteBtn}
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFieldSearch
|
||||
placeholder="Search..."
|
||||
fullWidth
|
||||
value={this.state.filter}
|
||||
onChange={(e) => {
|
||||
this.setState({
|
||||
filter: e.target.value
|
||||
}, this.fetchItems);
|
||||
}}
|
||||
data-test-subj="searchFilter"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
const tableColumns = [
|
||||
{
|
||||
field: 'title',
|
||||
name: 'Title',
|
||||
sortable: true,
|
||||
render: (field, record) => (
|
||||
<EuiLink
|
||||
className="dashboardLink"
|
||||
href={`#${createDashboardEditUrl(record.id)}`}
|
||||
data-test-subj={`dashboardListingTitleLink-${record.title.split(' ').join('-')}`}
|
||||
>
|
||||
{field}
|
||||
</EuiLink>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
dataType: 'string',
|
||||
sortable: true,
|
||||
}
|
||||
];
|
||||
if (!this.props.hideWriteControls) {
|
||||
tableColumns.push({
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
render: (record) => {
|
||||
return (
|
||||
<EuiLink
|
||||
href={`#${createDashboardEditUrl(record.id)}?_a=(viewMode:edit)`}
|
||||
>
|
||||
Edit
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
const pagination = {
|
||||
pageIndex: this.state.page,
|
||||
pageSize: this.state.perPage,
|
||||
totalItemCount: this.state.dashboards.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
};
|
||||
const selection = {
|
||||
itemId: 'id',
|
||||
onSelectionChange: (selection) => {
|
||||
this.setState({
|
||||
selectedIds: selection.map(item => { return item.id; })
|
||||
});
|
||||
}
|
||||
};
|
||||
const sorting = {};
|
||||
if (this.state.sortField) {
|
||||
sorting.sort = {
|
||||
field: this.state.sortField,
|
||||
direction: this.state.sortDirection,
|
||||
};
|
||||
}
|
||||
const items = this.state.dashboards.length === 0 ? [] : this.getPageOfItems();
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={items}
|
||||
loading={this.state.isFetchingItems}
|
||||
columns={tableColumns}
|
||||
selection={selection}
|
||||
noItemsMessage={this.renderNoItemsMessage()}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let createButton;
|
||||
if (!this.props.hideWriteControls) {
|
||||
createButton = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`}
|
||||
data-test-subj="newDashboardLink"
|
||||
>
|
||||
Create new dashboard
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiPage data-test-subj="dashboardLandingPage">
|
||||
|
||||
{this.state.showDeleteModal && this.renderConfirmDeleteModal()}
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd" data-test-subj="top-nav">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
Dashboard
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
{createButton}
|
||||
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{this.renderListingLimitWarning()}
|
||||
|
||||
{this.renderSearchBar()}
|
||||
|
||||
{this.renderTable()}
|
||||
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DashboardListing.propTypes = {
|
||||
find: PropTypes.func.isRequired,
|
||||
delete: PropTypes.func.isRequired,
|
||||
listingLimit: PropTypes.number.isRequired,
|
||||
hideWriteControls: PropTypes.bool.isRequired,
|
||||
initialFilter: PropTypes.string,
|
||||
};
|
||||
|
||||
DashboardListing.defaultProps = {
|
||||
initialFilter: EMPTY_FILTER,
|
||||
};
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
.dashboardListingTable {
|
||||
.kuiTableHeaderCell {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.actionBtnHeaderCell {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
jest.mock('ui/notify',
|
||||
() => ({
|
||||
toastNotifications: {
|
||||
addWarning: () => {},
|
||||
}
|
||||
}), { virtual: true });
|
||||
|
||||
jest.mock('lodash',
|
||||
() => ({
|
||||
// mock debounce to fire immediately with no internal timer
|
||||
debounce: function (func) {
|
||||
function debounced(...args) {
|
||||
return func.apply(this, args);
|
||||
}
|
||||
return debounced;
|
||||
}
|
||||
}), { virtual: true });
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
DashboardListing,
|
||||
} from './dashboard_listing';
|
||||
|
||||
const find = (num) => {
|
||||
const hits = [];
|
||||
for (let i = 0; i < num; i++) {
|
||||
hits.push({
|
||||
id: `dashboard${i}`,
|
||||
title: `dashboard${i} title`,
|
||||
description: `dashboard${i} desc`
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
total: num,
|
||||
hits: hits
|
||||
});
|
||||
};
|
||||
|
||||
test('renders table in loading state', () => {
|
||||
const component = shallow(<DashboardListing
|
||||
find={find.bind(null, 2)}
|
||||
delete={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('initialFilter', () => {
|
||||
const component = shallow(<DashboardListing
|
||||
find={find.bind(null, 2)}
|
||||
delete={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
initialFilter="my dashboard"
|
||||
/>);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('after fetch', () => {
|
||||
test('renders table rows', async () => {
|
||||
const component = shallow(<DashboardListing
|
||||
find={find.bind(null, 2)}
|
||||
delete={() => {}}
|
||||
listingLimit={1000}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders call to action when no dashboards exist', async () => {
|
||||
const component = shallow(<DashboardListing
|
||||
find={find.bind(null, 0)}
|
||||
delete={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('hideWriteControls', async () => {
|
||||
const component = shallow(<DashboardListing
|
||||
find={find.bind(null, 0)}
|
||||
delete={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={true}
|
||||
/>);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders warning when listingLimit is exceeded', async () => {
|
||||
const component = shallow(<DashboardListing
|
||||
find={find.bind(null, 2)}
|
||||
delete={() => {}}
|
||||
listingLimit={1}
|
||||
hideWriteControls={false}
|
||||
/>);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
<dashboard-listing
|
||||
find="find"
|
||||
delete="delete"
|
||||
listing-limit="listingLimit"
|
||||
hide-write-controls="hideWriteControls"
|
||||
initial-filter="initialFilter"
|
||||
/>
|
|
@ -42,8 +42,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
describe('delete', async function () {
|
||||
it('default confirm action is cancel', async function () {
|
||||
await PageObjects.dashboard.searchForDashboardWithName('');
|
||||
await PageObjects.dashboard.clickListItemCheckbox();
|
||||
await PageObjects.dashboard.searchForDashboardWithName(dashboardName);
|
||||
await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox();
|
||||
await PageObjects.dashboard.clickDeleteSelectedDashboards();
|
||||
|
||||
await PageObjects.common.pressEnterKey();
|
||||
|
@ -56,7 +56,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
|
||||
it('succeeds on confirmation press', async function () {
|
||||
await PageObjects.dashboard.clickListItemCheckbox();
|
||||
await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox();
|
||||
await PageObjects.dashboard.clickDeleteSelectedDashboards();
|
||||
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
|
@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
describe('search', function () {
|
||||
before(async () => {
|
||||
await PageObjects.dashboard.clearSearchValue();
|
||||
await PageObjects.dashboard.clickCreateDashboardPrompt();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.dashboard.saveDashboard('Two Words');
|
||||
});
|
||||
|
||||
|
|
|
@ -198,8 +198,13 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
|
|||
return await testSubjects.exists('createDashboardPromptButton');
|
||||
}
|
||||
|
||||
async clickListItemCheckbox() {
|
||||
await testSubjects.click('dashboardListItemCheckbox');
|
||||
async checkDashboardListingSelectAllCheckbox() {
|
||||
const element = await testSubjects.find('checkboxSelectAll');
|
||||
const isSelected = await element.isSelected();
|
||||
if (!isSelected) {
|
||||
log.debug(`checking checkbox "checkboxSelectAll"`);
|
||||
await testSubjects.click('checkboxSelectAll');
|
||||
}
|
||||
}
|
||||
|
||||
async clickDeleteSelectedDashboards() {
|
||||
|
@ -366,6 +371,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
|
|||
await retry.try(async () => {
|
||||
const searchFilter = await testSubjects.find('searchFilter');
|
||||
await searchFilter.clearValue();
|
||||
await PageObjects.common.pressEnterKey();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -385,13 +391,14 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
|
|||
await searchFilter.click();
|
||||
// Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed.
|
||||
await searchFilter.type(dashName.replace('-', ' '));
|
||||
await PageObjects.common.pressEnterKey();
|
||||
});
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
|
||||
async getCountOfDashboardsInListingTable() {
|
||||
const dashboardTitles = await testSubjects.findAll('dashboardListingRow');
|
||||
const dashboardTitles = await find.allByCssSelector('.dashboardLink');
|
||||
return dashboardTitles.length;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue