Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
331b013
add ff and tempates page with route
MykhailoKondrat Jan 26, 2026
ddd0734
add basic table
MykhailoKondrat Jan 27, 2026
1ff5388
add sorting and improvments
MykhailoKondrat Jan 27, 2026
8a1995b
add more actions, move code to new folder
MykhailoKondrat Jan 27, 2026
c6a3b5d
add crud for individual template--no-verify
MykhailoKondrat Jan 28, 2026
7b24081
add search, filters, bulk actions, panel
MykhailoKondrat Jan 28, 2026
71e7ab9
add tests, refactor structure
MykhailoKondrat Jan 29, 2026
adf5711
fix buttn lbl
MykhailoKondrat Jan 29, 2026
4f2be19
hotfix
MykhailoKondrat Jan 29, 2026
9504463
fix tests
MykhailoKondrat Jan 29, 2026
69cc637
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Jan 29, 2026
25e5c65
fix ts
MykhailoKondrat Feb 2, 2026
cee1a30
add import flyout placeholder
MykhailoKondrat Feb 2, 2026
1afdb0f
add go to edit tempalte logic
MykhailoKondrat Feb 2, 2026
031e4cd
remove preview option
MykhailoKondrat Feb 2, 2026
85da853
add delete tempalte logic
MykhailoKondrat Feb 2, 2026
455ef14
add set as default action
MykhailoKondrat Feb 2, 2026
8f80a7d
add clone template
MykhailoKondrat Feb 2, 2026
4aaed3a
add export template
MykhailoKondrat Feb 2, 2026
7738c7a
add bulk actions
MykhailoKondrat Feb 3, 2026
531ec1e
add reset pagination on bulk delete
MykhailoKondrat Feb 3, 2026
db98b89
add fix for bulk actions causing UI jump
MykhailoKondrat Feb 3, 2026
7a76611
add tags and authors filter
MykhailoKondrat Feb 4, 2026
137243e
add persitent table state
MykhailoKondrat Feb 4, 2026
842d2de
add new ff
MykhailoKondrat Feb 5, 2026
14ba180
fix types
MykhailoKondrat Feb 5, 2026
e1ad533
fix other types
MykhailoKondrat Feb 5, 2026
4df5a1f
Merge branch 'main' into feature/15529-add-cases-templates-page
MykhailoKondrat Feb 9, 2026
81a0463
align template type with schema and add more cols
MykhailoKondrat Feb 9, 2026
b84158d
fix test
MykhailoKondrat Feb 10, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const;
export const CASE_VIEW_ALERT_TABLE_PATH =
`${CASE_VIEW_PATH}/?tabId=${CASE_VIEW_PAGE_TABS.ALERTS}` as const;
export const CASE_VIEW_TAB_PATH = `${CASE_VIEW_PATH}/?tabId=:tabId` as const;

export const CASES_TEMPLATES_PATH = '/templates' as const;
export const CASES_CREATE_TEMPLATE_PATH = `${CASES_TEMPLATES_PATH}/create-template` as const;
export const CASES_EDIT_TEMPLATE_PATH = `${CASES_TEMPLATES_PATH}/:templateId/edit` as const;
/**
* The main Cases application is in the stack management under the
* Alerts and Insights section. To do that, Cases registers to the management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export const DEFAULT_FEATURES: CasesFeaturesAllRequired = Object.freeze({
metrics: [],
observables: { enabled: true, autoExtract: false },
events: { enabled: false },
templates: { enabled: false },
});

/**
Expand Down Expand Up @@ -257,6 +258,7 @@ export const LOCAL_STORAGE_KEYS = {
casesTableColumns: 'cases.list.tableColumns',
casesTableFiltersConfig: 'cases.list.tableFiltersConfig',
casesTableState: 'cases.list.state',
templatesTableState: 'templates.list.state',
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ export const TemplateSchema = z.object({
* Deletion date, used to indicate soft-deletion. Elastic uses strings, but will narrow it some more to actual dates here.
*/
deletedAt: z.string().datetime().nullable(),

/**
* Template description
*/
description: z.string().optional(),

/**
* Tags for categorization
*/
tags: z.array(z.string()).optional(),

/**
* Template author
*/
author: z.string().optional(),

/**
* Number of times this template has been used
*/
usageCount: z.number().optional(),

/**
* Number of fields in the template
*/
fieldCount: z.number().optional(),

/**
* Array of field names to display in a tooltip
*/
fieldNames: z.array(z.string()).optional(),

/**
* Last time this template was used
*/
lastUsedAt: z.string().datetime().optional(),

/**
* Whether this is the default template
*/
isDefault: z.boolean().optional(),
});

export type Template = z.infer<typeof TemplateSchema>;
Expand Down
3 changes: 3 additions & 0 deletions x-pack/platform/plugins/shared/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export interface CasesUiConfigType {
incrementalId: {
enabled: boolean;
};
templates: {
enabled: boolean;
};
}

export const UserActionTypeAll = 'all' as const;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';

import { APP_ID, CASES_CONFIGURE_PATH, CASES_CREATE_PATH } from '../../../common/constants';
import {
APP_ID,
CASES_CONFIGURE_PATH,
CASES_CREATE_PATH,
CASES_CREATE_TEMPLATE_PATH,
CASES_TEMPLATES_PATH,
} from '../../../common/constants';
import { useNavigation } from '../lib/kibana';
import type { ICasesDeepLinkId } from './deep_links';
import type { CaseViewPathParams, CaseViewPathSearchParams } from './paths';
import { generateCaseViewPath } from './paths';
import type { CaseViewPathParams, CaseViewPathSearchParams, TemplateViewPathParams } from './paths';
import { generateCaseViewPath, generateTemplateEditPath } from './paths';
import { stringifyToURL, parseURL } from '../../components/utils';
import { useApplication } from '../lib/kibana/use_application';

Expand Down Expand Up @@ -69,6 +75,8 @@ const navigationMapping = {
all: { path: '/' },
create: { path: CASES_CREATE_PATH },
configure: { path: CASES_CONFIGURE_PATH },
templates: { path: CASES_TEMPLATES_PATH },
createTemplate: { path: CASES_CREATE_TEMPLATE_PATH },
};

export const useAllCasesNavigation = () => {
Expand Down Expand Up @@ -96,6 +104,54 @@ export const useConfigureCasesNavigation = () => {
return { getConfigureCasesUrl, navigateToConfigureCases };
};

export const useCasesTemplatesNavigation = () => {
const [getCasesTemplatesUrl, navigateToCasesTemplates] = useCasesNavigation({
path: navigationMapping.templates.path,
deepLinkId: APP_ID,
});
return { getCasesTemplatesUrl, navigateToCasesTemplates };
};

export const useCasesCreateTemplateNavigation = () => {
const [getCasesCreateTemplateUrl, navigateToCasesCreateTemplate] = useCasesNavigation({
path: navigationMapping.createTemplate.path,
deepLinkId: APP_ID,
});
return { getCasesCreateTemplateUrl, navigateToCasesCreateTemplate };
};

export const useTemplateViewParams = () => useParams<TemplateViewPathParams>();

type GetEditTemplateUrl = (pathParams: TemplateViewPathParams, absolute?: boolean) => string;
type NavigateToEditTemplate = (pathParams: TemplateViewPathParams) => void;

export const useCasesEditTemplateNavigation = () => {
const { appId } = useApplication();
const { navigateTo, getAppUrl } = useNavigation(appId);
const deepLinkId = APP_ID;

const getCasesEditTemplateUrl = useCallback<GetEditTemplateUrl>(
(pathParams, absolute) =>
getAppUrl({
deepLinkId,
absolute,
path: generateTemplateEditPath(pathParams),
}),
[deepLinkId, getAppUrl]
);

const navigateToCasesEditTemplate = useCallback<NavigateToEditTemplate>(
(pathParams) =>
navigateTo({
deepLinkId,
path: generateTemplateEditPath(pathParams),
}),
[navigateTo, deepLinkId]
);

return { getCasesEditTemplateUrl, navigateToCasesEditTemplate };
};

type GetCaseViewUrl = (pathParams: CaseViewPathParams, absolute?: boolean) => string;
type NavigateToCaseView = (pathParams: CaseViewPathParams) => void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
CASE_VIEW_PATH,
CASE_VIEW_COMMENT_PATH,
CASE_VIEW_TAB_PATH,
CASES_TEMPLATES_PATH,
CASES_CREATE_TEMPLATE_PATH,
CASES_EDIT_TEMPLATE_PATH,
} from '../../../common/constants';
import type { CASE_VIEW_PAGE_TABS } from '../../../common/types';

Expand All @@ -38,7 +41,25 @@ export const getCaseViewPath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASE_VIEW_PATH}`);
export const getCaseViewWithCommentPath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASE_VIEW_COMMENT_PATH}`);
export const getCasesTemplatesPath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASES_TEMPLATES_PATH}`);
export const getCasesCreateTemplatePath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASES_CREATE_TEMPLATE_PATH}`);
export const getCasesEditTemplatePath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASES_EDIT_TEMPLATE_PATH}`);

export interface TemplateViewPathParams {
templateId: string;
}

export const generateTemplateEditPath = (params: TemplateViewPathParams): string => {
return normalizePath(
generatePath(
CASES_EDIT_TEMPLATE_PATH,
params as ExtractRouteParams<typeof CASES_EDIT_TEMPLATE_PATH>
)
);
};
export const generateCaseViewPath = (params: CaseViewPathParams): string => {
const { commentId, tabId } = params;
// paths with commentId have their own specific path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const useCasesFeatures = (): UseCasesFeatures => {
const { isAtLeastGold, isAtLeastPlatinum } = useLicense();
const hasLicenseGreaterThanPlatinum = isAtLeastPlatinum();
const hasLicenseWithAtLeastGold = isAtLeastGold();

const casesFeatures = useMemo(
() => ({
isAlertsEnabled: features.alerts.enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@ import {
getCaseViewWithCommentPath,
useAllCasesNavigation,
useCaseViewNavigation,
getCasesTemplatesPath,
getCasesCreateTemplatePath,
getCasesEditTemplatePath,
} from '../../common/navigation';
import { NoPrivilegesPage } from '../no_privileges';
import * as i18n from './translations';
import { useReadonlyHeader } from './use_readonly_header';
import type { CaseViewProps } from '../case_view/types';
import type { CreateCaseFormProps } from '../create/form';
import { TemplateFormPage } from '../templates_v2/pages/template_form_page';
import { KibanaServices } from '../../common/lib/kibana/services';

const CaseViewLazy: React.FC<CaseViewProps> = lazy(() => import('../case_view'));

const AllCasesTemplatesLazy: React.FC = lazy(
() => import('../templates_v2/pages/all_templates_page')
);
const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
actionsNavigation,
ruleDetailsNavigation,
Expand All @@ -46,12 +53,15 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
const { basePath, permissions } = useCasesContext();
const { navigateToAllCases } = useAllCasesNavigation();
const { navigateToCaseView } = useCaseViewNavigation();

useReadonlyHeader();

const onCreateCaseSuccess: CreateCaseFormProps['onSuccess'] = useCallback(
async ({ id }) => navigateToCaseView({ detailName: id }),
[navigateToCaseView]
);
const config = KibanaServices.getConfig();
const isTemplatesEnabled = config?.templates?.enabled ?? false;

return (
<>
Expand Down Expand Up @@ -81,6 +91,23 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
)}
</Route>

{isTemplatesEnabled && (
<Route exact path={getCasesTemplatesPath(basePath)}>
<Suspense fallback={<EuiLoadingSpinner />}>
<AllCasesTemplatesLazy />
</Suspense>
</Route>
)}
{isTemplatesEnabled && (
<Route exact path={getCasesCreateTemplatePath(basePath)}>
<TemplateFormPage />
</Route>
)}
{isTemplatesEnabled && (
<Route exact path={getCasesEditTemplatePath(basePath)}>
<TemplateFormPage />
</Route>
)}
{/* NOTE: current case view implementation retains some local state between renders, eg. when going from one case directly to another one. as a short term fix, we are forcing the component remount. */}
<Route exact path={[getCaseViewWithCommentPath(basePath), getCaseViewPath(basePath)]}>
<Suspense fallback={<EuiLoadingSpinner />}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EuiThemeComputed } from '@elastic/eui';
import {
EuiButton,
EuiCallOut,
EuiFlexItem,
EuiLink,
Expand All @@ -31,7 +32,7 @@ import type {
ActionConnector,
ObservableTypeConfiguration,
} from '../../../common/types/domain';
import { useKibana } from '../../common/lib/kibana';
import { KibanaServices, useKibana } from '../../common/lib/kibana';
import { useGetActionTypes } from '../../containers/configure/use_action_types';
import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';

Expand All @@ -44,7 +45,7 @@ import { getConnectorById, addOrReplaceField } from '../utils';
import { HeaderPage } from '../header_page';
import { useCasesContext } from '../cases_context/use_cases_context';
import { useCasesBreadcrumbs } from '../use_breadcrumbs';
import { CasesDeepLinkId } from '../../common/navigation';
import { CasesDeepLinkId, useCasesTemplatesNavigation } from '../../common/navigation';
import { CustomFields } from '../custom_fields';
import { CommonFlyout } from './flyout';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
Expand Down Expand Up @@ -122,6 +123,8 @@ export const ConfigureCases: React.FC = React.memo(() => {
const hasMinimumLicensePermissionsForObservables = license.isAtLeastPlatinum();

const { isObservablesFeatureEnabled } = useCasesFeatures();
const config = KibanaServices.getConfig();
const isTemplatesEnabled = config?.templates?.enabled ?? false;

const [connectorIsValid, setConnectorIsValid] = useState(true);
const [flyOutVisibility, setFlyOutVisibility] = useState<Flyout | null>(null);
Expand Down Expand Up @@ -178,7 +181,6 @@ export const ConfigureCases: React.FC = React.memo(() => {
},
[refetchActionTypes, refetchCaseConfigure, refetchConnectors, setEditedConnectorItem]
);

const onConnectorCreated = useCallback(
async (createdConnector: ActionConnector) => {
const caseConnector = normalizeActionConnector(createdConnector);
Expand Down Expand Up @@ -624,6 +626,8 @@ export const ConfigureCases: React.FC = React.memo(() => {
</CommonFlyout>
) : null;

const { navigateToCasesTemplates } = useCasesTemplatesNavigation();

return (
<EuiPageSection restrictWidth={true}>
<HeaderPage data-test-subj="case-configure-title" title={i18n.CONFIGURE_CASES_PAGE_TITLE} />
Expand Down Expand Up @@ -716,6 +720,18 @@ export const ConfigureCases: React.FC = React.memo(() => {
</EuiFlexItem>
</div>

{isTemplatesEnabled && (
<>
<EuiSpacer size="xl" />
<div css={sectionWrapperCss}>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => navigateToCasesTemplates()}>
{i18n.SHOW_ALL_TEMPLATES}
</EuiButton>
</EuiFlexItem>
</div>
</>
)}
{hasMinimumLicensePermissionsForObservables && isObservablesFeatureEnabled && (
<>
<EuiSpacer size="xl" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,10 @@ export const EDIT_OBSERVABLE_TYPE = i18n.translate(
defaultMessage: 'Edit observable type',
}
);

export const SHOW_ALL_TEMPLATES = i18n.translate(
'xpack.cases.configureCases.templates.showAllTemplates',
{
defaultMessage: 'Show all templates',
}
);
Loading
Loading