Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[7.x] [Ingest] Agent Config Details - Data sources list ui (#60429) #60586

Merged
merged 1 commit into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions x-pack/plugins/ingest_manager/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const datasourceRouteService = {
getUpdatePath: (datasourceId: string) => {
return DATASOURCE_API_ROUTES.UPDATE_PATTERN.replace('{datasourceId}', datasourceId);
},

getDeletePath: () => {
return DATASOURCE_API_ROUTES.DELETE_PATTERN;
},
};

export const agentConfigRouteService = {
Expand Down
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
Expand Up @@ -6,6 +6,10 @@
import { sendRequest } from './use_request';
import { datasourceRouteService } from '../../services';
import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types';
import {
DeleteDatasourcesRequest,
DeleteDatasourcesResponse,
} from '../../../../../common/types/rest_spec';

export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
return sendRequest<CreateDatasourceResponse>({
Expand All @@ -14,3 +18,11 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
body: JSON.stringify(body),
});
};

export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => {
return sendRequest<DeleteDatasourcesResponse>({
path: datasourceRouteService.getDeletePath(),
method: 'post',
body: JSON.stringify(body),
});
};
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,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>
);
};
Loading