-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Ingest] Agent Config Details - Data sources list ui (#60429)
* refactor `use_details_uri` hook and introduce `useAgentConfigLink` * Refactor structure for datasources view * Sync up table columns * Added row actions to Datasources list * Datasources table filters * Support deleting datasource action * Added PackageIcon to datasources list
- Loading branch information
1 parent
5389e1a
commit cba0af5
Showing
14 changed files
with
872 additions
and
307 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
...ications/ingest_manager/sections/agent_config/components/danger_eui_context_menu_item.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; | ||
`; |
237 changes: 237 additions & 0 deletions
237
...plications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
/* | ||
* 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, useConfig } 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[], | ||
onSuccess?: OnSuccessCallback | ||
) => void; | ||
|
||
type OnSuccessCallback = (datasourcesDeleted: string[]) => void; | ||
|
||
export const DatasourceDeleteProvider: React.FunctionComponent<Props> = ({ | ||
agentConfig, | ||
children, | ||
}) => { | ||
const { notifications } = useCore(); | ||
const { | ||
fleet: { enabled: isFleetEnabled }, | ||
} = useConfig(); | ||
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 || !isFleetEnabled) { | ||
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, isFleetEnabled, 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(); | ||
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> | ||
); | ||
}; |
Oops, something went wrong.