mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Investigation App] add basic table (#192016)
## Summary Relates to https://github.com/elastic/kibana/issues/191549 Adds basic investigation list page with pagination and delete action. <img width="1478" alt="Screenshot 2024-09-06 at 9 48 37 AM" src="https://github.com/user-attachments/assets/ce20db46-8da1-4f35-b374-d64c6e46e451">
This commit is contained in:
parent
0b980d8d9d
commit
9b13685565
6 changed files with 279 additions and 25 deletions
|
@ -9,6 +9,7 @@ import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
|||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { Route, Router, Routes } from '@kbn/shared-ux-router';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import type { History } from 'history';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
@ -18,6 +19,8 @@ import { getRoutes } from './routes/config';
|
|||
import { InvestigateAppServices } from './services/types';
|
||||
import type { InvestigateAppStartDependencies } from './types';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function Application({
|
||||
coreStart,
|
||||
history,
|
||||
|
@ -61,8 +64,6 @@ function Application({
|
|||
);
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
return (
|
||||
<KibanaThemeProvider theme={theme}>
|
||||
<InvestigateAppContextProvider context={context}>
|
||||
|
@ -71,6 +72,7 @@ function Application({
|
|||
<Router history={history}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</Router>
|
||||
</coreStart.i18n.Context>
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
export const investigationKeys = {
|
||||
all: ['investigation'] as const,
|
||||
list: (params: { page: number; perPage: number }) =>
|
||||
[...investigationKeys.all, 'list', params] as const,
|
||||
list: (params?: { page: number; perPage: number }) =>
|
||||
[...investigationKeys.all, 'list', ...(params ? [params] : [])] as const,
|
||||
fetch: (params: { id: string }) => [...investigationKeys.all, 'fetch', params] as const,
|
||||
notes: ['investigation', 'notes'] as const,
|
||||
fetchNotes: (params: { investigationId: string }) =>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useKibana } from './use_kibana';
|
||||
import { investigationKeys } from './query_key_factory';
|
||||
|
||||
type ServerError = IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
export function useDeleteInvestigation() {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
core: {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
return useMutation<void, ServerError, { investigationId: string }, { investigationId: string }>(
|
||||
['deleteInvestigation'],
|
||||
({ investigationId }) => {
|
||||
return http.delete<void>(`/api/observability/investigations/${investigationId}`, {
|
||||
version: '2023-10-31',
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: (response, {}) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.investigateApp.deleteInvestigationSuccess', {
|
||||
defaultMessage: 'Investigation deleted successfully',
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: investigationKeys.list(),
|
||||
exact: false,
|
||||
refetchType: 'all',
|
||||
});
|
||||
},
|
||||
onError: (error, {}, context) => {
|
||||
toasts.addError(
|
||||
new Error(
|
||||
error.body?.message ??
|
||||
i18n.translate('xpack.investigateApp.deleteInvestigationError', {
|
||||
defaultMessage: 'Unable to delete investigation: an error occurred',
|
||||
})
|
||||
),
|
||||
{
|
||||
title: i18n.translate('xpack.investigateApp.deleteInvestigationErrorTitle', {
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -4,21 +4,31 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { Criteria, EuiBasicTable, EuiBasicTableColumn, EuiLink, EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { InvestigationResponse } from '@kbn/investigation-shared/src/rest_specs/investigation';
|
||||
import { InvestigationListActions } from './investigation_list_actions';
|
||||
import { useFetchInvestigationList } from '../../../hooks/use_fetch_investigation_list';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { paths } from '../../../../common/paths';
|
||||
|
||||
export function InvestigationList() {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const {
|
||||
core: {
|
||||
http: { basePath },
|
||||
uiSettings,
|
||||
},
|
||||
} = useKibana();
|
||||
const { data, isLoading, isError } = useFetchInvestigationList();
|
||||
const { data, isLoading, isError } = useFetchInvestigationList({
|
||||
page: pageIndex + 1,
|
||||
perPage: pageSize,
|
||||
});
|
||||
const dateFormat = uiSettings.get('dateFormat');
|
||||
const tz = uiSettings.get('dateFormat:tz');
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
@ -41,23 +51,90 @@ export function InvestigationList() {
|
|||
}
|
||||
|
||||
const investigations = data?.results ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<InvestigationResponse>> = [
|
||||
{
|
||||
field: 'title',
|
||||
name: i18n.translate('xpack.investigateApp.investigationList.titleLabel', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
render: (title: InvestigationResponse['title'], investigation: InvestigationResponse) => {
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="investigateAppInvestigationListDirectLink"
|
||||
href={basePath.prepend(paths.investigationDetails(investigation.id))}
|
||||
>
|
||||
{title}
|
||||
</EuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdBy',
|
||||
name: i18n.translate('xpack.investigateApp.investigationList.createdByLabel', {
|
||||
defaultMessage: 'Created by',
|
||||
}),
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'notes',
|
||||
name: i18n.translate('xpack.investigateApp.investigationList.notesLabel', {
|
||||
defaultMessage: 'Comments',
|
||||
}),
|
||||
render: (notes: InvestigationResponse['notes']) => <span>{notes?.length || 0}</span>,
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
name: i18n.translate('xpack.investigateApp.investigationList.createdAtLabel', {
|
||||
defaultMessage: 'Created at',
|
||||
}),
|
||||
render: (createdAt: InvestigationResponse['createdAt']) => (
|
||||
<span>{moment(createdAt).tz(tz).format(dateFormat)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: 'Status',
|
||||
render: (status: InvestigationResponse['status']) => {
|
||||
const color = status === 'ongoing' ? 'danger' : 'success';
|
||||
return <EuiBadge color={color}>{status}</EuiBadge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
render: (investigation: InvestigationResponse) => (
|
||||
<InvestigationListActions investigation={investigation} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: total || 0,
|
||||
pageSizeOptions: [10, 50],
|
||||
showPerPageOptions: true,
|
||||
};
|
||||
|
||||
const onTableChange = ({ page }: Criteria<InvestigationResponse>) => {
|
||||
if (page) {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<ul>
|
||||
{investigations.map((investigation) => (
|
||||
<li key={investigation.id}>
|
||||
<EuiLink
|
||||
data-test-subj="investigateAppInvestigationListLink"
|
||||
href={basePath.prepend(paths.investigationDetails(investigation.id))}
|
||||
>
|
||||
{investigation.title}
|
||||
</EuiLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.investigateApp.investigationList.tableCaption', {
|
||||
defaultMessage: 'Investigations List',
|
||||
})}
|
||||
items={investigations}
|
||||
pagination={pagination}
|
||||
rowHeader="title"
|
||||
columns={columns}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiButtonEmpty,
|
||||
EuiToolTip,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeaderTitle,
|
||||
useGeneratedHtmlId,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { InvestigationResponse } from '@kbn/investigation-shared/src/rest_specs/investigation';
|
||||
import { useDeleteInvestigation } from '../../../hooks/use_delete_investigation';
|
||||
export function InvestigationListActions({
|
||||
investigation,
|
||||
}: {
|
||||
investigation: InvestigationResponse;
|
||||
}) {
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
const {
|
||||
mutate: deleteInvestigation,
|
||||
isLoading: isDeleting,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useDeleteInvestigation();
|
||||
const closeDeleteModal = () => setIsDeleteModalVisible(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError || isSuccess) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
}, [isError, isSuccess]);
|
||||
|
||||
const modalTitleId = useGeneratedHtmlId();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeleteModalVisible && (
|
||||
<EuiModal aria-labelledby={modalTitleId} onClose={closeDeleteModal}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle id={modalTitleId}>
|
||||
{i18n.translate('xpack.investigateApp.deleteModal.title', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
{i18n.translate('xpack.investigateApp.deleteModal.description', {
|
||||
defaultMessage: "You can't recover this investigation after deletion.",
|
||||
})}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={closeDeleteModal}
|
||||
data-test-subj="investigateAppInvestigationListDeleteModalCancelButton"
|
||||
>
|
||||
{i18n.translate('xpack.investigateApp.deleteModal.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="investigateAppInvestigationListDeleteModalConfirmButton"
|
||||
onClick={() => {
|
||||
deleteInvestigation({ investigationId: investigation.id });
|
||||
}}
|
||||
fill
|
||||
color="danger"
|
||||
isLoading={isDeleting}
|
||||
>
|
||||
{i18n.translate('xpack.investigateApp.deleteModal.confirm', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
)}
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.investigateApp.investigationList.deleteAction', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="investigateAppInvestigationListDeleteButton"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.investigateApp.investigationList.deleteAction.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete investigation "{name}"',
|
||||
values: { name: investigation.title },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
onClick={() => setIsDeleteModalVisible(true)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue