Skip to content

Commit

Permalink
feat(rca): add external incident url (elastic#193919)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdelemme authored Sep 25, 2024
1 parent 5e2a575 commit 5d51da9
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 28 deletions.
1 change: 1 addition & 0 deletions packages/kbn-investigation-shared/src/rest_specs/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const createInvestigationParamsSchema = z.object({
}),
origin: z.union([alertOriginSchema, blankOriginSchema]),
tags: z.array(z.string()),
externalIncidentUrl: z.string().nullable(),
}),
});

Expand Down
1 change: 1 addition & 0 deletions packages/kbn-investigation-shared/src/rest_specs/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const updateInvestigationParamsSchema = z.object({
timeRange: z.object({ from: z.number(), to: z.number() }),
}),
tags: z.array(z.string()),
externalIncidentUrl: z.string().nullable(),
})
.partial(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const investigationSchema = z.object({
tags: z.array(z.string()),
notes: z.array(investigationNoteSchema),
items: z.array(investigationItemSchema),
externalIncidentUrl: z.string().nullable(),
});

type Status = z.infer<typeof statusSchema>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { EuiFormRow, EuiFieldText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { InvestigationForm } from '../investigation_edit_form';

const I18N_LABEL = i18n.translate(
'xpack.investigateApp.investigationEditForm.externalIncidentUrlLabel',
{ defaultMessage: 'External incident URL' }
);

export function ExternalIncidentField() {
const { control, getFieldState } = useFormContext<InvestigationForm>();

return (
<EuiFormRow
fullWidth
isInvalid={getFieldState('externalIncidentUrl').invalid}
label={I18N_LABEL}
>
<Controller
name="externalIncidentUrl"
control={control}
rules={{ required: false }}
render={({ field: { ref, ...field }, fieldState }) => (
<EuiFieldText
{...field}
value={field.value || ''}
data-test-subj="investigateAppExternalIncidentFieldFieldText"
fullWidth
isInvalid={fieldState.invalid}
placeholder={I18N_LABEL}
/>
)}
/>
</EuiFormRow>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { CreateInvestigationParams, UpdateInvestigationParams } from '@kbn/investigation-shared';
import { v4 as uuidv4 } from 'uuid';
import type { InvestigationForm } from './investigation_edit_form';

export function toCreateInvestigationParams(data: InvestigationForm): CreateInvestigationParams {
return {
id: uuidv4(),
title: data.title,
params: {
timeRange: {
from: new Date(new Date().getTime() - 30 * 60 * 1000).getTime(),
to: new Date().getTime(),
},
},
tags: data.tags,
origin: {
type: 'blank',
},
externalIncidentUrl:
data.externalIncidentUrl && data.externalIncidentUrl.trim().length > 0
? data.externalIncidentUrl
: null,
};
}

export function toUpdateInvestigationParams(data: InvestigationForm): UpdateInvestigationParams {
return {
title: data.title,
status: data.status,
tags: data.tags,
externalIncidentUrl:
data.externalIncidentUrl && data.externalIncidentUrl.trim().length > 0
? data.externalIncidentUrl
: null,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,22 @@ import { InvestigationResponse } from '@kbn/investigation-shared';
import { pick } from 'lodash';
import React from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { v4 as uuidv4 } from 'uuid';
import { paths } from '../../../common/paths';
import { useCreateInvestigation } from '../../hooks/use_create_investigation';
import { useFetchInvestigation } from '../../hooks/use_fetch_investigation';
import { useKibana } from '../../hooks/use_kibana';
import { useUpdateInvestigation } from '../../hooks/use_update_investigation';
import { InvestigationNotFound } from '../investigation_not_found/investigation_not_found';
import { ExternalIncidentField } from './fields/external_incident_field';
import { StatusField } from './fields/status_field';
import { TagsField } from './fields/tags_field';
import { toCreateInvestigationParams, toUpdateInvestigationParams } from './form_helper';

export interface InvestigationForm {
title: string;
status: InvestigationResponse['status'];
tags: string[];
externalIncidentUrl: string | null;
}

interface Props {
Expand All @@ -64,8 +66,17 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) {
const { mutateAsync: createInvestigation } = useCreateInvestigation();

const methods = useForm<InvestigationForm>({
defaultValues: { title: 'New investigation', status: 'triage', tags: [] },
values: investigation ? pick(investigation, ['title', 'status', 'tags']) : undefined,
defaultValues: {
title: i18n.translate('xpack.investigateApp.investigationDetailsPage.newInvestigationLabel', {
defaultMessage: 'New investigation',
}),
status: 'triage',
tags: [],
externalIncidentUrl: null,
},
values: investigation
? pick(investigation, ['title', 'status', 'tags', 'externalIncidentUrl'])
: undefined,
mode: 'all',
});

Expand All @@ -81,24 +92,11 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) {
if (isEditing) {
await updateInvestigation({
investigationId: investigationId!,
payload: { title: data.title, status: data.status, tags: data.tags },
payload: toUpdateInvestigationParams(data),
});
onClose();
} else {
const resp = await createInvestigation({
id: uuidv4(),
title: data.title,
params: {
timeRange: {
from: new Date(new Date().getTime() - 30 * 60 * 1000).getTime(),
to: new Date().getTime(),
},
},
tags: data.tags,
origin: {
type: 'blank',
},
});
const resp = await createInvestigation(toCreateInvestigationParams(data));
navigateToUrl(basePath.prepend(paths.investigationDetails(resp.id)));
}
};
Expand Down Expand Up @@ -157,6 +155,9 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) {
<EuiFlexItem grow>
<TagsField />
</EuiFlexItem>
<EuiFlexItem grow>
<ExternalIncidentField />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutBody>
<EuiFlyoutFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function InvestigationDetails({ user }: Props) {
],
}}
>
<EuiFlexGroup direction="row">
<EuiFlexGroup direction="row" responsive>
<EuiFlexItem grow={8}>
<InvestigationItems />
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { EuiButtonEmpty, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useInvestigation } from '../../contexts/investigation_context';

export function ExternalIncidentButton() {
const { investigation } = useInvestigation();

if (!investigation?.externalIncidentUrl) {
return null;
}

return (
<EuiButtonEmpty
data-test-subj="externalIncidentHeaderButton"
iconType="link"
size="xs"
href={investigation.externalIncidentUrl}
target="_blank"
>
<EuiText size="s">
{i18n.translate('xpack.investigateApp.investigationHeader.externalIncidentTextLabel', {
defaultMessage: 'External incident',
})}
</EuiText>
</EuiButtonEmpty>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
*/

import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { formatDistance } from 'date-fns';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { InvestigationStatusBadge } from '../../../../components/investigation_status_badge/investigation_status_badge';
import { InvestigationTag } from '../../../../components/investigation_tag/investigation_tag';
import { useInvestigation } from '../../contexts/investigation_context';
import { AlertDetailsButton } from './alert_details_button';
import { ExternalIncidentButton } from './external_incident_button';

export function InvestigationHeader() {
const { investigation } = useInvestigation();
Expand Down Expand Up @@ -62,6 +63,12 @@ export function InvestigationHeader() {
/>
</EuiText>
</EuiFlexItem>

{!!investigation.externalIncidentUrl && (
<EuiFlexItem grow={false}>
<ExternalIncidentButton />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function InvestigationItems() {

return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<InvestigationSearchBar
dateRangeFrom={globalParams.timeRange.from}
dateRangeTo={globalParams.timeRange.to}
Expand All @@ -31,11 +31,11 @@ export function InvestigationItems() {
updateInvestigationParams({ timeRange: nextTimeRange });
}}
/>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<InvestigationItemsList />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<InvestigationItemsList />
</EuiFlexItem>

<AddInvestigationItem />
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ export function investigationRepositoryFactory({
logger: Logger;
}): InvestigationRepository {
function toInvestigation(stored: StoredInvestigation): Investigation | undefined {
const result = investigationSchema.safeParse({
...stored,
});
const result = investigationSchema.safeParse(stored);

if (!result.success) {
logger.error(`Invalid stored Investigation with id [${stored.id}]`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export function HeaderActions({
type: 'alert',
id: alert.fields[ALERT_UUID],
},
externalIncidentUrl: null,
},
});

Expand Down

0 comments on commit 5d51da9

Please sign in to comment.