-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Ingest] Agent Config Details - Data sources list ui #60429
Changes from 16 commits
5c9d80f
9784c6c
108ce9e
834f3c5
9350a05
c7c5bd0
37f944c
9d0d9b6
cc40ff2
74f3860
aa7af7e
ccb1698
eee5766
e7e6be2
89a2529
aded059
d64759a
874f773
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import React, { useEffect, useMemo, useState } from 'react'; | ||
import { ICON_TYPES, EuiIcon, EuiIconProps } from '@elastic/eui'; | ||
import { PackageInfo, PackageListItem } from '../../../../common/types/models'; | ||
import { useLinks } from '../sections/epm/hooks'; | ||
import { epmRouteService } from '../../../../common/services'; | ||
import { sendRequest } from '../hooks/use_request'; | ||
import { GetInfoResponse } from '../types'; | ||
type Package = PackageInfo | PackageListItem; | ||
|
||
const CACHED_ICONS = new Map<string, string>(); | ||
|
||
export const PackageIcon: React.FunctionComponent<{ | ||
packageName: string; | ||
version?: string; | ||
icons?: Package['icons']; | ||
} & Omit<EuiIconProps, 'type'>> = ({ packageName, version, icons, ...euiIconProps }) => { | ||
const iconType = usePackageIcon(packageName, version, icons); | ||
return <EuiIcon size="s" type={iconType} {...euiIconProps} />; | ||
}; | ||
|
||
const usePackageIcon = (packageName: string, version?: string, icons?: Package['icons']) => { | ||
const { toImage } = useLinks(); | ||
const [iconType, setIconType] = useState<string>(''); | ||
const pkgKey = `${packageName}-${version ?? ''}`; | ||
|
||
// Generates an icon path or Eui Icon name based on an icon list from the package | ||
// or by using the package name against logo icons from Eui | ||
const fromInput = useMemo(() => { | ||
return (iconList?: Package['icons']) => { | ||
const svgIcons = iconList?.filter(iconDef => iconDef.type === 'image/svg+xml'); | ||
const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src; | ||
if (localIconSrc) { | ||
CACHED_ICONS.set(pkgKey, toImage(localIconSrc)); | ||
setIconType(CACHED_ICONS.get(pkgKey) as string); | ||
return; | ||
} | ||
|
||
const euiLogoIcon = ICON_TYPES.find(key => key.toLowerCase() === `logo${packageName}`); | ||
if (euiLogoIcon) { | ||
CACHED_ICONS.set(pkgKey, euiLogoIcon); | ||
setIconType(euiLogoIcon); | ||
return; | ||
} | ||
|
||
CACHED_ICONS.set(pkgKey, 'package'); | ||
setIconType('package'); | ||
}; | ||
}, [packageName, pkgKey, toImage]); | ||
|
||
useEffect(() => { | ||
if (CACHED_ICONS.has(pkgKey)) { | ||
setIconType(CACHED_ICONS.get(pkgKey) as string); | ||
return; | ||
} | ||
|
||
// Use API to see if package has icons defined | ||
if (!icons && version !== undefined) { | ||
fromPackageInfo(pkgKey) | ||
.catch(() => undefined) // ignore API errors | ||
.then(fromInput); | ||
} else { | ||
fromInput(icons); | ||
} | ||
}, [icons, toImage, packageName, version, fromInput, pkgKey]); | ||
|
||
return iconType; | ||
}; | ||
|
||
const fromPackageInfo = async (pkgKey: string) => { | ||
const { data } = await sendRequest<GetInfoResponse>({ | ||
path: epmRouteService.getInfoPath(pkgKey), | ||
method: 'get', | ||
}); | ||
return data?.response?.icons; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import styled from 'styled-components'; | ||
import { EuiContextMenuItem } from '@elastic/eui'; | ||
|
||
export const DangerEuiContextMenuItem = styled(EuiContextMenuItem)` | ||
color: ${props => props.theme.eui.textColors.danger}; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import React, { Fragment, useMemo, useRef, useState } from 'react'; | ||
import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { FormattedMessage } from '@kbn/i18n/react'; | ||
import { useCore, sendRequest, sendDeleteDatasource } from '../../../hooks'; | ||
import { AGENT_API_ROUTES } from '../../../../../../common/constants'; | ||
import { AgentConfig } from '../../../../../../common/types/models'; | ||
|
||
interface Props { | ||
agentConfig: AgentConfig; | ||
children: (deleteDatasourcePrompt: DeleteAgentConfigDatasourcePrompt) => React.ReactElement; | ||
} | ||
|
||
export type DeleteAgentConfigDatasourcePrompt = ( | ||
datasourcesToDelete: string[], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that we plan on surfacing bulk delete of data sources anywhere in the UI so the code path for deleting multiple data sources in this file won't ever be triggered, but I'm ok with leaving the code in |
||
onSuccess?: OnSuccessCallback | ||
) => void; | ||
|
||
type OnSuccessCallback = (datasourcesDeleted: string[]) => void; | ||
|
||
export const DatasourceDeleteProvider: React.FunctionComponent<Props> = ({ | ||
agentConfig, | ||
children, | ||
}) => { | ||
const { notifications } = useCore(); | ||
const [datasources, setDatasources] = useState<string[]>([]); | ||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); | ||
const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState<boolean>(false); | ||
const [agentsCount, setAgentsCount] = useState<number>(0); | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
const onSuccessCallback = useRef<OnSuccessCallback | null>(null); | ||
|
||
const fetchAgentsCount = useMemo( | ||
() => async () => { | ||
if (isLoadingAgentsCount) { | ||
return; | ||
} | ||
setIsLoadingAgentsCount(true); | ||
const { data } = await sendRequest<{ total: number }>({ | ||
path: AGENT_API_ROUTES.LIST_PATTERN, | ||
method: 'get', | ||
query: { | ||
page: 1, | ||
perPage: 1, | ||
kuery: `agents.config_id : ${agentConfig.id}`, | ||
}, | ||
}); | ||
setAgentsCount(data?.total || 0); | ||
setIsLoadingAgentsCount(false); | ||
}, | ||
[agentConfig.id, isLoadingAgentsCount] | ||
); | ||
|
||
const deleteDatasourcesPrompt = useMemo( | ||
(): DeleteAgentConfigDatasourcePrompt => (datasourcesToDelete, onSuccess = () => undefined) => { | ||
if (!Array.isArray(datasourcesToDelete) || datasourcesToDelete.length === 0) { | ||
throw new Error('No datasources specified for deletion'); | ||
} | ||
setIsModalOpen(true); | ||
setDatasources(datasourcesToDelete); | ||
fetchAgentsCount(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking # of agents would only be needed if fleet is enabled There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh - yes. Will add check for it. |
||
onSuccessCallback.current = onSuccess; | ||
}, | ||
[fetchAgentsCount] | ||
); | ||
|
||
const closeModal = useMemo( | ||
() => () => { | ||
setDatasources([]); | ||
setIsLoading(false); | ||
setIsLoadingAgentsCount(false); | ||
setIsModalOpen(false); | ||
}, | ||
[] | ||
); | ||
|
||
const deleteDatasources = useMemo( | ||
() => async () => { | ||
setIsLoading(true); | ||
|
||
try { | ||
const { data } = await sendDeleteDatasource({ datasourceIds: datasources }); | ||
const successfulResults = data?.filter(result => result.success) || []; | ||
const failedResults = data?.filter(result => !result.success) || []; | ||
|
||
if (successfulResults.length) { | ||
const hasMultipleSuccesses = successfulResults.length > 1; | ||
const successMessage = hasMultipleSuccesses | ||
? i18n.translate( | ||
'xpack.ingestManager.deleteDatasource.successMultipleNotificationTitle', | ||
{ | ||
defaultMessage: 'Deleted {count} data sources', | ||
values: { count: successfulResults.length }, | ||
} | ||
) | ||
: i18n.translate( | ||
'xpack.ingestManager.deleteDatasource.successSingleNotificationTitle', | ||
{ | ||
defaultMessage: "Deleted data source '{id}'", | ||
values: { id: successfulResults[0].id }, | ||
} | ||
); | ||
notifications.toasts.addSuccess(successMessage); | ||
} | ||
|
||
if (failedResults.length) { | ||
const hasMultipleFailures = failedResults.length > 1; | ||
const failureMessage = hasMultipleFailures | ||
? i18n.translate( | ||
'xpack.ingestManager.deleteDatasource.failureMultipleNotificationTitle', | ||
{ | ||
defaultMessage: 'Error deleting {count} data sources', | ||
values: { count: failedResults.length }, | ||
} | ||
) | ||
: i18n.translate( | ||
'xpack.ingestManager.deleteDatasource.failureSingleNotificationTitle', | ||
{ | ||
defaultMessage: "Error deleting data source '{id}'", | ||
values: { id: failedResults[0].id }, | ||
} | ||
); | ||
notifications.toasts.addDanger(failureMessage); | ||
} | ||
|
||
if (onSuccessCallback.current) { | ||
onSuccessCallback.current(successfulResults.map(result => result.id)); | ||
} | ||
} catch (e) { | ||
notifications.toasts.addDanger( | ||
i18n.translate('xpack.ingestManager.deleteDatasource.fatalErrorNotificationTitle', { | ||
defaultMessage: 'Error deleting data source', | ||
}) | ||
); | ||
} | ||
closeModal(); | ||
}, | ||
[closeModal, datasources, notifications.toasts] | ||
); | ||
|
||
const renderModal = () => { | ||
if (!isModalOpen) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<EuiOverlayMask> | ||
<EuiConfirmModal | ||
title={ | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.deleteMultipleTitle" | ||
defaultMessage="Delete {count, plural, one {data source} other {# data sources}}?" | ||
values={{ count: datasources.length }} | ||
/> | ||
} | ||
onCancel={closeModal} | ||
onConfirm={deleteDatasources} | ||
cancelButtonText={ | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel" | ||
defaultMessage="Cancel" | ||
/> | ||
} | ||
confirmButtonText={ | ||
isLoading || isLoadingAgentsCount ? ( | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.loadingButtonLabel" | ||
defaultMessage="Loading…" | ||
/> | ||
) : ( | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.confirmButtonLabel" | ||
defaultMessage="Delete {agentConfigsCount, plural, one {data source} other {data sources}}" | ||
values={{ | ||
agentConfigsCount: datasources.length, | ||
}} | ||
/> | ||
) | ||
} | ||
buttonColor="danger" | ||
confirmButtonDisabled={isLoading || isLoadingAgentsCount} | ||
> | ||
{isLoadingAgentsCount ? ( | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.loadingAgentsCountMessage" | ||
defaultMessage="Checking affected agents…" | ||
/> | ||
) : agentsCount ? ( | ||
<> | ||
<EuiCallOut | ||
color="danger" | ||
title={ | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle" | ||
defaultMessage="This action will affect {agentsCount} {agentsCount, plural, one {agent} other {agents}}." | ||
values={{ agentsCount }} | ||
/> | ||
} | ||
> | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage" | ||
defaultMessage="Fleet has detected that {agentConfigName} is already in use by some of your agents." | ||
values={{ | ||
agentConfigName: <strong>{agentConfig.name}</strong>, | ||
}} | ||
/> | ||
</EuiCallOut> | ||
<EuiSpacer size="l" /> | ||
</> | ||
) : null} | ||
{!isLoadingAgentsCount && ( | ||
<FormattedMessage | ||
id="xpack.ingestManager.deleteDatasource.confirmModal.generalMessage" | ||
defaultMessage="This action can not be undone. Are you sure you wish to continue?" | ||
/> | ||
)} | ||
</EuiConfirmModal> | ||
</EuiOverlayMask> | ||
); | ||
}; | ||
|
||
return ( | ||
<Fragment> | ||
{children(deleteDatasourcesPrompt)} | ||
{renderModal()} | ||
</Fragment> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should
/epm/components/package_icon.tsx
be deleted and usages replaced with this one?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was waiting for a review before making the changes. I will do that refactoring after merging this PR :)