Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ monaco-promql
oxlint
react-element-to-jsx-string
react-day-picker
mermaid
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@
"maplibre-gl": "5.3.0",
"markdown-it": "14.1.1",
"memoize-one": "6.0.0",
"mermaid": "11.13.0",
"mime": "2.6.0",
"mime-types": "2.1.35",
"minimatch": "10.2.4",
Expand Down
21 changes: 21 additions & 0 deletions typings/mermaid.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

/* eslint-disable import/no-default-export */

declare module 'mermaid' {
interface MermaidAPI {
initialize: (config: Record<string, unknown>) => void;
render: (id: string, text: string) => Promise<{ svg: string }>;
parse: (text: string) => Promise<unknown>;
}

const mermaid: MermaidAPI;
export default mermaid;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ export enum AttachmentType {
text = 'text',
esql = 'esql',
visualization = 'visualization',
mermaid = 'mermaid',
}

interface AttachmentDataMap {
[AttachmentType.esql]: EsqlAttachmentData;
[AttachmentType.text]: TextAttachmentData;
[AttachmentType.screenContext]: ScreenContextAttachmentData;
[AttachmentType.visualization]: VisualizationAttachmentData;
[AttachmentType.mermaid]: MermaidAttachmentData;
}

export const esqlAttachmentDataSchema = z.object({
Expand Down Expand Up @@ -145,4 +147,19 @@ export interface VisualizationOriginData {
description?: string;
}

export const mermaidAttachmentDataSchema = z.object({
content: z.string(),
title: z.string().optional(),
});

/**
* Data for a mermaid diagram attachment.
*/
export interface MermaidAttachmentData {
/** Mermaid diagram source code */
content: string;
/** Optional title for the diagram */
title?: string;
}

export type AttachmentDataOf<Type extends AttachmentType> = AttachmentDataMap[Type];
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type TextAttachment = Attachment<AttachmentType.text>;
export type ScreenContextAttachment = Attachment<AttachmentType.screenContext>;
export type EsqlAttachment = Attachment<AttachmentType.esql>;
export type VisualizationAttachment = Attachment<AttachmentType.visualization>;
export type MermaidAttachment = Attachment<AttachmentType.mermaid>;

/**
* Input version of an attachment, where the id is optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type {
ScreenContextAttachment,
EsqlAttachment,
VisualizationAttachment,
MermaidAttachment,
} from './attachments';
export {
AttachmentType,
Expand All @@ -21,13 +22,15 @@ export {
screenContextAttachmentDataSchema,
visualizationAttachmentDataSchema,
visualizationOriginDataSchema,
mermaidAttachmentDataSchema,
type TextAttachmentData,
type ScreenContextAttachmentData,
type ScreenContextTimeRange,
screenContextTimeRangeSchema,
type EsqlAttachmentData,
type VisualizationAttachmentData,
type VisualizationOriginData,
type MermaidAttachmentData,
} from './attachment_types';

export type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const platformCoreTools = {
getWorkflowExecutionStatus: platformCoreTool('get_workflow_execution_status'),
productDocumentation: platformCoreTool('product_documentation'),
cases: platformCoreTool('cases'),
addCaseAttachment: platformCoreTool('add_case_attachment'),
integrationKnowledge: platformCoreTool('integration_knowledge'),
} as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type SkillsDirectoryStructure = Directory<{
skills: Directory<{
platform: FileDirectory<{
dashboard: FileDirectory;
mermaid: FileDirectory;
visualization: FileDirectory;
workflows: FileDirectory;
}>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export const MERMAID_CASE_ATTACHMENT_TYPE = 'agentBuilderMermaid';
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependsOn:
- '@kbn/core-http-server-mocks'
- '@kbn/core-ui-settings-server-mocks'
- '@kbn/share-plugin'
- '@kbn/i18n-react'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* 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 React, { useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiButton,
EuiButtonEmpty,
EuiSelectable,
EuiLoadingSpinner,
EuiText,
EuiFieldSearch,
EuiSpacer,
useGeneratedHtmlId,
} from '@elastic/eui';
import type { EuiSelectableOption } from '@elastic/eui';
import type { HttpStart, NotificationsStart } from '@kbn/core/public';

const CASES_FIND_URL = '/api/cases/_find';
const CASES_INTERNAL_BULK_CREATE_ATTACHMENTS_URL =
'/internal/cases/{case_id}/attachments/_bulk_create';

interface CaseSummary {
id: string;
title: string;
status: string;
}

interface AddToCasePickerProps {
http: HttpStart;
notifications?: NotificationsStart;
mermaidContent: string;
mermaidTitle?: string;
attachmentType: string;
onClose: () => void;
}

export const AddToCasePicker: React.FC<AddToCasePickerProps> = ({
http,
notifications,
mermaidContent,
mermaidTitle,
attachmentType,
onClose,
}) => {
const modalTitleId = useGeneratedHtmlId();
const [cases, setCases] = useState<CaseSummary[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCaseId, setSelectedCaseId] = useState<string | null>(null);

const fetchCases = useCallback(
async (search?: string) => {
setLoading(true);
setError(null);
try {
const query: Record<string, string | number> = {
page: 1,
perPage: 20,
sortField: 'updatedAt',
sortOrder: 'desc',
status: 'open',
};

if (search) {
query.search = search;
}

const response = await http.get<{ cases: CaseSummary[] }>(CASES_FIND_URL, { query });
setCases(response.cases);
} catch (e) {
const message =
e instanceof Error
? e.message
: i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.fetchCasesError', {
defaultMessage: 'Failed to fetch cases',
});
setError(message);
} finally {
setLoading(false);
}
},
[http]
);

useEffect(() => {
fetchCases();
}, [fetchCases]);

const handleSearch = useCallback(
(value: string) => {
setSearchQuery(value);
fetchCases(value);
},
[fetchCases]
);

const handleSelectionChange = useCallback((options: EuiSelectableOption[]) => {
const selected = options.find((opt) => opt.checked === 'on');
setSelectedCaseId(selected?.key ?? null);
}, []);

const handleSubmit = useCallback(async () => {
if (!selectedCaseId) {
return;
}

setSubmitting(true);
try {
const url = CASES_INTERNAL_BULK_CREATE_ATTACHMENTS_URL.replace(
'{case_id}',
encodeURIComponent(selectedCaseId)
);
await http.post(url, {
body: JSON.stringify([
{
type: attachmentType,
data: {
content: mermaidContent,
...(mermaidTitle ? { title: mermaidTitle } : {}),
},
},
]),
});

notifications?.toasts.addSuccess(
i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.addToCaseSuccess', {
defaultMessage: 'Mermaid diagram added to case',
})
);
onClose();
} catch (e) {
const message =
e instanceof Error
? e.message
: i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.addToCaseError', {
defaultMessage: 'Failed to add diagram to case',
});
notifications?.toasts.addDanger(message);
} finally {
setSubmitting(false);
}
}, [selectedCaseId, http, attachmentType, mermaidContent, mermaidTitle, notifications, onClose]);

const selectableOptions: EuiSelectableOption[] = cases.map((c) => ({
key: c.id,
label: c.title,
checked: c.id === selectedCaseId ? 'on' : undefined,
}));

return (
<EuiModal onClose={onClose} style={{ maxWidth: 500 }} aria-labelledby={modalTitleId}>
<EuiModalHeader>
<EuiModalHeaderTitle id={modalTitleId}>
{i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.addToCaseTitle', {
defaultMessage: 'Add diagram to case',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFieldSearch
placeholder={i18n.translate(
'xpack.agentBuilderPlatform.attachments.mermaid.searchCasesPlaceholder',
{ defaultMessage: 'Search cases…' }
)}
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
isClearable
fullWidth
/>
<EuiSpacer size="s" />
{loading ? (
<EuiLoadingSpinner size="l" />
) : error ? (
<EuiText color="danger" size="s">
{error}
</EuiText>
) : cases.length === 0 ? (
<EuiText size="s" color="subdued">
{i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.noCasesFound', {
defaultMessage: 'No open cases found',
})}
</EuiText>
) : (
<EuiSelectable
options={selectableOptions}
singleSelection
onChange={handleSelectionChange}
listProps={{ bordered: true }}
>
{(list) => list}
</EuiSelectable>
)}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onClose}>
{i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.cancel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
<EuiButton
fill
onClick={handleSubmit}
isLoading={submitting}
isDisabled={!selectedCaseId || loading}
>
{i18n.translate('xpack.agentBuilderPlatform.attachments.mermaid.addToCase', {
defaultMessage: 'Add to case',
})}
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@
* 2.0.
*/

import type { HttpStart, NotificationsStart } from '@kbn/core/public';
import type { AttachmentServiceStartContract } from '@kbn/agent-builder-browser';
import type { ILocatorClient } from '@kbn/share-plugin/common/url_service';
import { AttachmentType } from '@kbn/agent-builder-common/attachments';
import { createEsqlAttachmentDefinition } from './esql_attachment';
import { textAttachmentDefinition } from './text_attachment';
import { screenContextAttachmentDefinition } from './screen_context_attachment';
import { createMermaidAttachmentDefinition } from './mermaid_attachment';

export const registerAttachmentUiDefinitions = ({
attachments,
locators,
http,
notifications,
}: {
attachments: AttachmentServiceStartContract;
locators: ILocatorClient;
http: HttpStart;
notifications: NotificationsStart;
}) => {
attachments.addAttachmentType(AttachmentType.text, textAttachmentDefinition);
attachments.addAttachmentType(AttachmentType.screenContext, screenContextAttachmentDefinition);
attachments.addAttachmentType(AttachmentType.esql, createEsqlAttachmentDefinition({ locators }));
attachments.addAttachmentType(
AttachmentType.mermaid,
createMermaidAttachmentDefinition({ http, notifications })
);
};
Loading
Loading