[Ingest] Edit datasource UI (#64727)

* Adjust NewDatasource type to exclude stream `agent_stream` property, add additional datasource hooks

* Initial pass at edit datasource UI

* Clean up dupe code, fix submit button not enabled after re-selecting a package

* Remove delete config functionality from list page

* Show validation errors for data source name and description fields

* Fix types

* Add success toasts

* Review fixes, clean up i18n

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jen Huang 2020-04-29 18:03:30 -07:00 committed by GitHub
parent 9b65cbd92b
commit fba5128bd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 462 additions and 198 deletions

View file

@ -3,11 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Datasource, NewDatasource, DatasourceInput } from '../types';
import { Datasource, DatasourceInput } from '../types';
import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource';
describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
const mockNewDatasource: NewDatasource = {
const mockDatasource: Datasource = {
id: 'some-uuid',
name: 'mock-datasource',
description: '',
config_id: '',
@ -15,11 +16,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
output_id: '',
namespace: 'default',
inputs: [],
};
const mockDatasource: Datasource = {
...mockNewDatasource,
id: 'some-uuid',
revision: 1,
};
@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
});
});
it('uses name for id when id is not provided in case of new datasource', () => {
expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({
id: 'mock-datasource',
name: 'mock-datasource',
namespace: 'default',
enabled: true,
use_output: 'default',
inputs: [],
});
});
it('returns agent datasource config with flattened input and package stream', () => {
expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({
id: 'some-uuid',

View file

@ -3,16 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types';
import { Datasource, FullAgentConfigDatasource } from '../types';
import { DEFAULT_OUTPUT } from '../constants';
export const storedDatasourceToAgentDatasource = (
datasource: Datasource | NewDatasource
datasource: Datasource
): FullAgentConfigDatasource => {
const { name, namespace, enabled, package: pkg, inputs } = datasource;
const { id, name, namespace, enabled, package: pkg, inputs } = datasource;
const fullDatasource: FullAgentConfigDatasource = {
id: 'id' in datasource ? datasource.id : name,
id: id || name,
name,
namespace,
enabled,

View file

@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectAttributes } from 'src/core/public';
import {
Datasource,
DatasourcePackage,
@ -26,7 +24,7 @@ export interface NewAgentConfig {
monitoring_enabled?: Array<'logs' | 'metrics'>;
}
export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes {
export interface AgentConfig extends NewAgentConfig {
id: string;
status: AgentConfigStatus;
datasources: string[] | Datasource[];

View file

@ -17,22 +17,29 @@ export interface DatasourceConfigRecordEntry {
export type DatasourceConfigRecord = Record<string, DatasourceConfigRecordEntry>;
export interface DatasourceInputStream {
export interface NewDatasourceInputStream {
id: string;
enabled: boolean;
dataset: string;
processors?: string[];
config?: DatasourceConfigRecord;
vars?: DatasourceConfigRecord;
}
export interface DatasourceInputStream extends NewDatasourceInputStream {
agent_stream?: any;
}
export interface DatasourceInput {
export interface NewDatasourceInput {
type: string;
enabled: boolean;
processors?: string[];
config?: DatasourceConfigRecord;
vars?: DatasourceConfigRecord;
streams: NewDatasourceInputStream[];
}
export interface DatasourceInput extends Omit<NewDatasourceInput, 'streams'> {
streams: DatasourceInputStream[];
}
@ -44,10 +51,11 @@ export interface NewDatasource {
enabled: boolean;
package?: DatasourcePackage;
output_id: string;
inputs: DatasourceInput[];
inputs: NewDatasourceInput[];
}
export type Datasource = NewDatasource & {
export interface Datasource extends Omit<NewDatasource, 'inputs'> {
id: string;
inputs: DatasourceInput[];
revision: number;
};
}

View file

@ -5,12 +5,18 @@
*/
import { sendRequest, useRequest } from './use_request';
import { datasourceRouteService } from '../../services';
import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types';
import {
CreateDatasourceRequest,
CreateDatasourceResponse,
UpdateDatasourceRequest,
UpdateDatasourceResponse,
} from '../../types';
import {
DeleteDatasourcesRequest,
DeleteDatasourcesResponse,
GetDatasourcesRequest,
GetDatasourcesResponse,
GetOneDatasourceResponse,
} from '../../../../../common/types/rest_spec';
export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
});
};
export const sendUpdateDatasource = (
datasourceId: string,
body: UpdateDatasourceRequest['body']
) => {
return sendRequest<UpdateDatasourceResponse>({
path: datasourceRouteService.getUpdatePath(datasourceId),
method: 'put',
body: JSON.stringify(body),
});
};
export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => {
return sendRequest<DeleteDatasourcesResponse>({
path: datasourceRouteService.getDeletePath(),
@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) {
query,
});
}
export const sendGetOneDatasource = (datasourceId: string) => {
return sendRequest<GetOneDatasourceResponse>({
path: datasourceRouteService.getInfoPath(datasourceId),
method: 'get',
});
};

View file

@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{
<EuiFlexItem>
<EuiText>
<h1>
<FormattedMessage
id="xpack.ingestManager.createDatasource.pageTitle"
defaultMessage="Add data source"
/>
{from === 'edit' ? (
<FormattedMessage
id="xpack.ingestManager.editDatasource.pageTitle"
defaultMessage="Edit data source"
/>
) : (
<FormattedMessage
id="xpack.ingestManager.createDatasource.pageTitle"
defaultMessage="Add data source"
/>
)}
</h1>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{from === 'config' ? (
{from === 'edit' ? (
<FormattedMessage
id="xpack.ingestManager.editDatasource.pageDescription"
defaultMessage="Follow the instructions below to edit this data source."
/>
) : from === 'config' ? (
<FormattedMessage
id="xpack.ingestManager.createDatasource.pageDescriptionfromConfig"
defaultMessage="Follow the instructions below to add an integration to this agent configuration."
@ -68,7 +80,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{
<EuiFlexGroup justifyContent="flexEnd" direction={'row'} gutterSize="xl">
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
{agentConfig && from === 'config' ? (
{agentConfig && (from === 'config' || from === 'edit') ? (
<EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage

View file

@ -28,7 +28,7 @@ import {
} from '../../../hooks';
import { useLinks as useEPMLinks } from '../../epm/hooks';
import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components';
import { CreateDatasourceFrom } from './types';
import { CreateDatasourceFrom, DatasourceFormState } from './types';
import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services';
import { StepSelectPackage } from './step_select_package';
import { StepSelectConfig } from './step_select_config';
@ -85,6 +85,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => {
if (updatedPackageInfo) {
setPackageInfo(updatedPackageInfo);
setFormState('VALID');
} else {
setFormState('INVALID');
setPackageInfo(undefined);
@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL;
// Save datasource
const [formState, setFormState] = useState<
'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'
>('INVALID');
const [formState, setFormState] = useState<DatasourceFormState>('INVALID');
const saveDatasource = async () => {
setFormState('LOADING');
const result = await sendCreateDatasource(datasource);
@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const { error } = await saveDatasource();
if (!error) {
history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`);
notifications.toasts.addSuccess({
title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', {
defaultMessage: `Successfully added '{datasourceName}'`,
values: {
datasourceName: datasource.name,
},
}),
text:
agentCount && agentConfig
? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', {
defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`,
values: {
agentConfigName: agentConfig.name,
},
})
: undefined,
});
} else {
notifications.toasts.addError(error, {
title: 'Error',
@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}
validationResults={validationResults!}
/>
) : null,
},
@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
children:
agentConfig && packageInfo ? (
<StepConfigureDatasource
agentConfig={agentConfig}
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}

View file

@ -3,7 +3,7 @@
* 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 } from 'react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiPanel,
@ -15,75 +15,21 @@ import {
EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
AgentConfig,
PackageInfo,
Datasource,
NewDatasource,
DatasourceInput,
} from '../../../types';
import { PackageInfo, NewDatasource, DatasourceInput } from '../../../types';
import { Loading } from '../../../components';
import { packageToConfigDatasourceInputs } from '../../../services';
import { DatasourceValidationResults, validationHasErrors } from './services';
import { DatasourceInputPanel } from './components';
export const StepConfigureDatasource: React.FunctionComponent<{
agentConfig: AgentConfig;
packageInfo: PackageInfo;
datasource: NewDatasource;
updateDatasource: (fields: Partial<NewDatasource>) => void;
validationResults: DatasourceValidationResults;
submitAttempted: boolean;
}> = ({
agentConfig,
packageInfo,
datasource,
updateDatasource,
validationResults,
submitAttempted,
}) => {
// Form show/hide states
}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => {
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
// Update datasource's package and config info
useEffect(() => {
const dsPackage = datasource.package;
const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : '';
const pkgKey = `${packageInfo.name}-${packageInfo.version}`;
// If package has changed, create shell datasource with input&stream values based on package info
if (currentPkgKey !== pkgKey) {
// Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name
const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`);
const dsWithMatchingNames = (agentConfig.datasources as Datasource[])
.filter(ds => Boolean(ds.name.match(dsPackageNamePattern)))
.map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10))
.sort();
updateDatasource({
name: `${packageInfo.name}-${
dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1
}`,
package: {
name: packageInfo.name,
title: packageInfo.title,
version: packageInfo.version,
},
inputs: packageToConfigDatasourceInputs(packageInfo),
});
}
// If agent config has changed, update datasource's config ID and namespace
if (datasource.config_id !== agentConfig.id) {
updateDatasource({
config_id: agentConfig.id,
namespace: agentConfig.namespace,
});
}
}, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
// Step B, configure inputs (and their streams)
// Configure inputs (and their streams)
// Assume packages only export one datasource for now
const renderConfigureInputs = () =>
packageInfo.datasources &&

View file

@ -17,13 +17,16 @@ import {
} from '@elastic/eui';
import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types';
import { packageToConfigDatasourceInputs } from '../../../services';
import { Loading } from '../../../components';
import { DatasourceValidationResults } from './services';
export const StepDefineDatasource: React.FunctionComponent<{
agentConfig: AgentConfig;
packageInfo: PackageInfo;
datasource: NewDatasource;
updateDatasource: (fields: Partial<NewDatasource>) => void;
}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => {
validationResults: DatasourceValidationResults;
}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => {
// Form show/hide states
const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState<boolean>(false);
@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{
}
}, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
return (
return validationResults ? (
<>
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFormRow
isInvalid={!!validationResults.name}
error={validationResults.name}
label={
<FormattedMessage
id="xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel"
@ -102,6 +107,8 @@ export const StepDefineDatasource: React.FunctionComponent<{
/>
</EuiText>
}
isInvalid={!!validationResults.description}
error={validationResults.description}
>
<EuiFieldText
value={datasource.description}
@ -161,5 +168,7 @@ export const StepDefineDatasource: React.FunctionComponent<{
</Fragment>
) : null}
</>
) : (
<Loading />
);
};

View file

@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export type CreateDatasourceFrom = 'package' | 'config';
export type CreateDatasourceFrom = 'package' | 'config' | 'edit';
export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED';

View file

@ -19,7 +19,7 @@ import {
import { AgentConfig, Datasource } from '../../../../../types';
import { TableRowActions } from '../../../components/table_row_actions';
import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item';
import { useCapabilities } from '../../../../../hooks';
import { useCapabilities, useLink } from '../../../../../hooks';
import { useAgentConfigLink } from '../../hooks/use_details_uri';
import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider';
import { useConfigRefresh } from '../../hooks/use_config';
@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({
}) => {
const hasWriteCapabilities = useCapabilities().write;
const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id });
const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`);
const refreshConfig = useConfigRefresh();
// With the datasources provided on input, generate the list of datasources
@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({
<TableRowActions
items={[
// FIXME: implement View datasource action
// <EuiContextMenuItem
// disabled
// icon="inspect"
// onClick={() => {}}
// key="datasourceView"
// >
// <FormattedMessage
// id="xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle"
// defaultMessage="View data source"
// />
// </EuiContextMenuItem>,
<EuiContextMenuItem
disabled
icon="inspect"
onClick={() => {}}
key="datasourceView"
>
<FormattedMessage
id="xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle"
defaultMessage="View data source"
/>
</EuiContextMenuItem>,
// FIXME: implement Edit datasource action
<EuiContextMenuItem
disabled
disabled={!hasWriteCapabilities}
icon="pencil"
onClick={() => {}}
href={`${editDatasourceLink}/${datasource.id}`}
key="datasourceEdit"
>
<FormattedMessage
@ -225,12 +225,12 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({
/>
</EuiContextMenuItem>,
// FIXME: implement Copy datasource action
<EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="datasourceCopy">
<FormattedMessage
id="xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle"
defaultMessage="Copy data source"
/>
</EuiContextMenuItem>,
// <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="datasourceCopy">
// <FormattedMessage
// id="xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle"
// defaultMessage="Copy data source"
// />
// </EuiContextMenuItem>,
<DatasourceDeleteProvider agentConfig={config} key="datasourceDelete">
{deleteDatasourcePrompt => {
return (
@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({
],
},
],
[config, hasWriteCapabilities, refreshConfig]
[config, editDatasourceLink, hasWriteCapabilities, refreshConfig]
);
return (

View file

@ -0,0 +1,323 @@
/*
* 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, { useState, useEffect } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonEmpty,
EuiButton,
EuiSteps,
EuiBottomBar,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants';
import { AgentConfig, PackageInfo, NewDatasource } from '../../../types';
import {
useLink,
useCore,
useConfig,
sendUpdateDatasource,
sendGetAgentStatus,
sendGetOneAgentConfig,
sendGetOneDatasource,
sendGetPackageInfoByKey,
} from '../../../hooks';
import { Loading, Error } from '../../../components';
import {
CreateDatasourcePageLayout,
ConfirmCreateDatasourceModal,
} from '../create_datasource_page/components';
import {
DatasourceValidationResults,
validateDatasource,
validationHasErrors,
} from '../create_datasource_page/services';
import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types';
import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource';
import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource';
export const EditDatasourcePage: React.FunctionComponent = () => {
const { notifications } = useCore();
const {
fleet: { enabled: isFleetEnabled },
} = useConfig();
const {
params: { configId, datasourceId },
} = useRouteMatch();
const history = useHistory();
// Agent config, package info, and datasource states
const [isLoadingData, setIsLoadingData] = useState<boolean>(true);
const [loadingError, setLoadingError] = useState<Error>();
const [agentConfig, setAgentConfig] = useState<AgentConfig>();
const [packageInfo, setPackageInfo] = useState<PackageInfo>();
const [datasource, setDatasource] = useState<NewDatasource>({
name: '',
description: '',
config_id: '',
enabled: true,
output_id: '',
inputs: [],
});
// Retrieve agent config, package, and datasource info
useEffect(() => {
const getData = async () => {
setIsLoadingData(true);
setLoadingError(undefined);
try {
const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([
sendGetOneAgentConfig(configId),
sendGetOneDatasource(datasourceId),
]);
if (agentConfigData?.item) {
setAgentConfig(agentConfigData.item);
}
if (datasourceData?.item) {
const { id, revision, inputs, ...restOfDatasource } = datasourceData.item;
// Remove `agent_stream` from all stream info, we assign this after saving
const newDatasource = {
...restOfDatasource,
inputs: inputs.map(input => {
const { streams, ...restOfInput } = input;
return {
...restOfInput,
streams: streams.map(stream => {
const { agent_stream, ...restOfStream } = stream;
return restOfStream;
}),
};
}),
};
setDatasource(newDatasource);
if (datasourceData.item.package) {
const { data: packageData } = await sendGetPackageInfoByKey(
`${datasourceData.item.package.name}-${datasourceData.item.package.version}`
);
if (packageData?.response) {
setPackageInfo(packageData.response);
setValidationResults(validateDatasource(newDatasource, packageData.response));
setFormState('VALID');
}
}
}
} catch (e) {
setLoadingError(e);
}
setIsLoadingData(false);
};
getData();
}, [configId, datasourceId]);
// Retrieve agent count
const [agentCount, setAgentCount] = useState<number>(0);
useEffect(() => {
const getAgentCount = async () => {
const { data } = await sendGetAgentStatus({ configId });
if (data?.results.total) {
setAgentCount(data.results.total);
}
};
if (isFleetEnabled) {
getAgentCount();
}
}, [configId, isFleetEnabled]);
// Datasource validation state
const [validationResults, setValidationResults] = useState<DatasourceValidationResults>();
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
// Update datasource method
const updateDatasource = (updatedFields: Partial<NewDatasource>) => {
const newDatasource = {
...datasource,
...updatedFields,
};
setDatasource(newDatasource);
// eslint-disable-next-line no-console
console.debug('Datasource updated', newDatasource);
const newValidationResults = updateDatasourceValidation(newDatasource);
const hasValidationErrors = newValidationResults
? validationHasErrors(newValidationResults)
: false;
if (!hasValidationErrors) {
setFormState('VALID');
}
};
const updateDatasourceValidation = (newDatasource?: NewDatasource) => {
if (packageInfo) {
const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo);
setValidationResults(newValidationResult);
// eslint-disable-next-line no-console
console.debug('Datasource validation results', newValidationResult);
return newValidationResult;
}
};
// Cancel url
const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`);
const cancelUrl = CONFIG_URL;
// Save datasource
const [formState, setFormState] = useState<DatasourceFormState>('INVALID');
const saveDatasource = async () => {
setFormState('LOADING');
const result = await sendUpdateDatasource(datasourceId, datasource);
setFormState('SUBMITTED');
return result;
};
const onSubmit = async () => {
if (formState === 'VALID' && hasErrors) {
setFormState('INVALID');
return;
}
if (agentCount !== 0 && formState !== 'CONFIRM') {
setFormState('CONFIRM');
return;
}
const { error } = await saveDatasource();
if (!error) {
history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`);
notifications.toasts.addSuccess({
title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', {
defaultMessage: `Successfully updated '{datasourceName}'`,
values: {
datasourceName: datasource.name,
},
}),
text:
agentCount && agentConfig
? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', {
defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`,
values: {
agentConfigName: agentConfig.name,
},
})
: undefined,
});
} else {
notifications.toasts.addError(error, {
title: 'Error',
});
setFormState('VALID');
}
};
const layoutProps = {
from: 'edit' as CreateDatasourceFrom,
cancelUrl,
agentConfig,
packageInfo,
};
return (
<CreateDatasourcePageLayout {...layoutProps}>
{isLoadingData ? (
<Loading />
) : loadingError || !agentConfig || !packageInfo ? (
<Error
title={
<FormattedMessage
id="xpack.ingestManager.editDatasource.errorLoadingDataTitle"
defaultMessage="Error loading data"
/>
}
error={
loadingError ||
i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', {
defaultMessage: 'There was an error loading this data source information',
})
}
/>
) : (
<>
{formState === 'CONFIRM' && (
<ConfirmCreateDatasourceModal
agentCount={agentCount}
agentConfig={agentConfig}
onConfirm={onSubmit}
onCancel={() => setFormState('VALID')}
/>
)}
<EuiSteps
steps={[
{
title: i18n.translate(
'xpack.ingestManager.editDatasource.stepDefineDatasourceTitle',
{
defaultMessage: 'Define your data source',
}
),
children: (
<StepDefineDatasource
agentConfig={agentConfig}
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}
validationResults={validationResults!}
/>
),
},
{
title: i18n.translate(
'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle',
{
defaultMessage: 'Select the data you want to collect',
}
),
children: (
<StepConfigureDatasource
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
),
},
]}
/>
<EuiSpacer size="l" />
<EuiBottomBar css={{ zIndex: 5 }} paddingSize="s">
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" href={cancelUrl}>
<FormattedMessage
id="xpack.ingestManager.editDatasource.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
isLoading={formState === 'LOADING'}
disabled={formState !== 'VALID'}
iconType="save"
color="primary"
fill
>
<FormattedMessage
id="xpack.ingestManager.editDatasource.saveButton"
defaultMessage="Save data source"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
</>
)}
</CreateDatasourcePageLayout>
);
};

View file

@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { AgentConfigListPage } from './list_page';
import { AgentConfigDetailsPage } from './details_page';
import { CreateDatasourcePage } from './create_datasource_page';
import { EditDatasourcePage } from './edit_datasource_page';
export const AgentConfigApp: React.FunctionComponent = () => (
<Router>
<Switch>
<Route path="/configs/:configId/edit-datasource/:datasourceId">
<EditDatasourcePage />
</Route>
<Route path="/configs/:configId/add-datasource">
<CreateDatasourcePage />
</Route>

View file

@ -36,13 +36,11 @@ import {
useConfig,
useUrlParams,
} from '../../../hooks';
import { AgentConfigDeleteProvider } from '../components';
import { CreateAgentConfigFlyout } from './components';
import { SearchBar } from '../../../components/search_bar';
import { LinkedAgentCount } from '../components';
import { useAgentConfigLink } from '../details_page/hooks/use_details_uri';
import { TableRowActions } from '../components/table_row_actions';
import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item';
const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({
overflow: 'hidden',
@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>(
defaultMessage="Create data source"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem disabled={true} icon="copy" key="copyConfig">
<FormattedMessage
id="xpack.ingestManager.agentConfigList.copyConfigActionText"
defaultMessage="Copy configuration"
/>
</EuiContextMenuItem>,
<AgentConfigDeleteProvider key="deleteConfig">
{deleteAgentConfigsPrompt => {
return (
<DangerEuiContextMenuItem
icon="trash"
disabled={Boolean(config.is_default)}
onClick={() => deleteAgentConfigsPrompt([config.id], onDelete)}
>
<FormattedMessage
id="xpack.ingestManager.agentConfigList.deleteConfigActionText"
defaultMessage="Delete Configuration"
/>
</DangerEuiContextMenuItem>
);
}}
</AgentConfigDeleteProvider>,
// <EuiContextMenuItem disabled={true} icon="copy" key="copyConfig">
// <FormattedMessage
// id="xpack.ingestManager.agentConfigList.copyConfigActionText"
// defaultMessage="Copy configuration"
// />
// </EuiContextMenuItem>,
]}
/>
);
@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
: urlParams.kuery ?? ''
);
const { pagination, pageSizeOptions, setPagination } = usePagination();
const [selectedAgentConfigs, setSelectedAgentConfigs] = useState<AgentConfig[]>([]);
const history = useHistory();
const isCreateAgentConfigFlyoutOpen = 'create' in urlParams;
const setIsCreateAgentConfigFlyoutOpen = useCallback(
@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
/>
) : null}
<EuiFlexGroup alignItems={'center'} gutterSize="m">
{selectedAgentConfigs.length ? (
<EuiFlexItem>
<AgentConfigDeleteProvider>
{deleteAgentConfigsPrompt => (
<EuiButton
color="danger"
onClick={() => {
deleteAgentConfigsPrompt(
selectedAgentConfigs.map(agentConfig => agentConfig.id),
() => {
sendRequest();
setSelectedAgentConfigs([]);
}
);
}}
>
<FormattedMessage
id="xpack.ingestManager.agentConfigList.deleteButton"
defaultMessage="Delete {count, plural, one {# agent config} other {# agent configs}}"
values={{
count: selectedAgentConfigs.length,
}}
/>
</EuiButton>
)}
</AgentConfigDeleteProvider>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={4}>
<SearchBar
value={search}
@ -405,13 +356,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
items={agentConfigData ? agentConfigData.items : []}
itemId="id"
columns={columns}
isSelectable={true}
selection={{
selectable: (agentConfig: AgentConfig) => !agentConfig.is_default,
onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => {
setSelectedAgentConfigs(newSelectedAgentConfigs);
},
}}
isSelectable={false}
pagination={{
pageIndex: pagination.currentPage - 1,
pageSize: pagination.pageSize,

View file

@ -32,6 +32,8 @@ export {
// API schemas - Datasource
CreateDatasourceRequest,
CreateDatasourceResponse,
UpdateDatasourceRequest,
UpdateDatasourceResponse,
// API schemas - Data Streams
GetDataStreamsResponse,
// API schemas - Agents

View file

@ -22,6 +22,7 @@ import {
} from '../../types';
import {
GetAgentConfigsResponse,
GetAgentConfigsResponseItem,
GetOneAgentConfigResponse,
CreateAgentConfigResponse,
UpdateAgentConfigResponse,
@ -46,7 +47,7 @@ export const getAgentConfigsHandler: RequestHandler<
await bluebird.map(
items,
agentConfig =>
(agentConfig: GetAgentConfigsResponseItem) =>
listAgents(soClient, {
showInactive: true,
perPage: 0,

View file

@ -8225,11 +8225,8 @@
"xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成",
"xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント",
"xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去",
"xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー",
"xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成",
"xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース",
"xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除",
"xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除",
"xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明",
"xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...",
"xpack.ingestManager.agentConfigList.nameColumnTitle": "名前",
@ -8313,7 +8310,6 @@
"xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」",
"xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません",
"xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション",
"xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー",
"xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除",
"xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明",
"xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集",
@ -8321,7 +8317,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム",
"xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示",
"xpack.ingestManager.configDetails.subTabs.datasouces": "データソース",
"xpack.ingestManager.configDetails.subTabs.settings": "設定",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル",

View file

@ -8228,11 +8228,8 @@
"xpack.ingestManager.agentConfigList.addButton": "创建代理配置",
"xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理",
"xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选",
"xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置",
"xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源",
"xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源",
"xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}",
"xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置",
"xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述",
"xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……",
"xpack.ingestManager.agentConfigList.nameColumnTitle": "名称",
@ -8316,7 +8313,6 @@
"xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”",
"xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”",
"xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作",
"xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源",
"xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源",
"xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述",
"xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源",
@ -8324,7 +8320,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数",
"xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源",
"xpack.ingestManager.configDetails.subTabs.datasouces": "数据源",
"xpack.ingestManager.configDetails.subTabs.settings": "设置",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件",