Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ToolDefinitionWithSchema,
SerializedOnechatError,
} from '@kbn/onechat-common';
import type { ToolResult } from '@kbn/onechat-common/tools/tool_result';

export interface ListToolsResponse {
results: ToolDefinitionWithSchema[];
Expand Down Expand Up @@ -50,3 +51,7 @@ export type BulkDeleteToolResult = BulkDeleteToolSuccessResult | BulkDeleteToolF
export interface BulkDeleteToolResponse {
results: BulkDeleteToolResult[];
}

export interface ExecuteToolResponse {
result: ToolResult[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
import { css } from '@emotion/react';
import type { EsqlToolDefinitionWithSchema } from '@kbn/onechat-common';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { FormProvider } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
import type {
CreateToolPayload,
CreateToolResponse,
Expand All @@ -36,6 +37,7 @@ import {
import { OnechatEsqlToolForm, OnechatEsqlToolFormMode } from './form/esql_tool_form';
import type { OnechatEsqlToolFormData } from './form/types/esql_tool_form_types';
import { useEsqlToolForm } from '../../../hooks/tools/use_esql_tool_form';
import { OnechatTestFlyout } from '../execute/test_tools';

interface EsqlToolBaseProps {
tool?: EsqlToolDefinitionWithSchema;
Expand Down Expand Up @@ -65,8 +67,11 @@ export const EsqlTool: React.FC<EsqlToolProps> = ({
const { euiTheme } = useEuiTheme();
const { navigateToOnechatUrl } = useNavigation();
const form = useEsqlToolForm();
const { reset, formState } = form;
const { errors } = formState;
const { reset, formState, watch } = form;
const { errors, isDirty } = formState;
const [showTestFlyout, setShowTestFlyout] = useState(false);

const currentToolId = watch('name');

const handleClear = useCallback(() => {
reset();
Expand All @@ -86,7 +91,10 @@ export const EsqlTool: React.FC<EsqlToolProps> = ({

useEffect(() => {
if (tool) {
reset(transformEsqlToolToFormData(tool), { keepDefaultValues: true });
reset(transformEsqlToolToFormData(tool), {
keepDefaultValues: true,
keepDirty: true,
});
}
}, [tool, reset]);

Expand All @@ -107,6 +115,43 @@ export const EsqlTool: React.FC<EsqlToolProps> = ({
</EuiButton>
);

const saveAndTestButton = (
<EuiButton
fill
onClick={async () => {
const formData = form.getValues();
if (mode === OnechatEsqlToolFormMode.Edit) {
await saveTool(transformEsqlFormDataForUpdate(formData));
} else {
await saveTool(transformEsqlFormDataForCreate(formData));
}
if (currentToolId) {
setShowTestFlyout(true);
}
}}
disabled={Object.keys(errors).length > 0 || isSubmitting}
isLoading={isSubmitting}
>
{i18n.translate('xpack.onechat.tools.esqlToolFlyout.saveAndTestButtonLabel', {
defaultMessage: 'Save and Test',
})}
</EuiButton>
);

const testButton = (
<EuiButton
fill
onClick={() => {
setShowTestFlyout(true);
}}
disabled={Object.keys(errors).length > 0}
>
{i18n.translate('xpack.onechat.tools.esqlToolFlyout.testButtonLabel', {
defaultMessage: 'Test',
})}
</EuiButton>
);

return (
<FormProvider {...form}>
<KibanaPageTemplate>
Expand All @@ -127,7 +172,22 @@ export const EsqlTool: React.FC<EsqlToolProps> = ({
<EuiLoadingSpinner size="xxl" />
</EuiFlexGroup>
) : (
<OnechatEsqlToolForm mode={mode} formId={esqlToolFormId} saveTool={handleSave} />
<>
<OnechatEsqlToolForm mode={mode} formId={esqlToolFormId} saveTool={handleSave} />
{showTestFlyout && currentToolId && (
<OnechatTestFlyout
isOpen={showTestFlyout}
isLoading={isLoading}
toolId={currentToolId}
onClose={() => {
setShowTestFlyout(false);
if (mode === OnechatEsqlToolFormMode.Create) {
navigateToOnechatUrl(appPaths.tools.list);
}
}}
/>
)}
</>
)}
</KibanaPageTemplate.Section>
<KibanaPageTemplate.BottomBar
Expand All @@ -139,6 +199,8 @@ export const EsqlTool: React.FC<EsqlToolProps> = ({
<EuiFlexItem>
<EuiButton onClick={handleClear}>{labels.tools.clearButtonLabel}</EuiButton>
</EuiFlexItem>
{!isDirty && <EuiFlexItem>{testButton}</EuiFlexItem>}
{isDirty && <EuiFlexItem>{saveAndTestButton}</EuiFlexItem>}
<EuiFlexItem>
{Object.keys(errors).length > 0 ? (
<EuiToolTip display="block" content={labels.tools.saveButtonTooltip}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
* 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 {
EuiButton,
EuiFieldText,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiForm,
EuiFormRow,
EuiSpacer,
EuiTitle,
EuiCodeBlock,
} from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useState } from 'react';
import { useForm, FormProvider, Controller } from 'react-hook-form';
import type { ToolDefinition } from '@kbn/onechat-common';
import { i18n } from '@kbn/i18n';
import { useExecuteTool } from '../../../hooks/tools/use_execute_tools';
import type { ExecuteToolResponse } from '../../../../../common/http_api/tools';
import { useTool } from '../../../hooks/tools/use_tools';

interface OnechatTestToolFlyout {
isOpen: boolean;
isLoading?: boolean;
toolId: string;
onClose: () => void;
}

interface ToolParameter {
name: string;
label: string;
value: string;
type: string;
}

const getParameters = (tool: ToolDefinition | undefined): Array<ToolParameter> => {
if (!tool) return [];

const fields: Array<ToolParameter> = [];
if (tool.configuration && tool.configuration.params) {
const params = tool.configuration.params as Record<string, any>;
Object.entries(params).forEach(([paramName, paramConfig]) => {
fields.push({
name: paramName,
label: paramName,
value: '',
type: paramConfig.type || 'text',
});
});
}

return fields;
};

export const OnechatTestFlyout: React.FC<OnechatTestToolFlyout> = ({ toolId, onClose }) => {
const [response, setResponse] = useState<string>('{}');

const form = useForm<Record<string, any>>({
mode: 'onChange',
});

const {
handleSubmit,
formState: { errors },
} = form;

const { tool } = useTool({ toolId });

const { executeTool, isLoading: isExecuting } = useExecuteTool({
onSuccess: (data: ExecuteToolResponse) => {
setResponse(JSON.stringify(data, null, 2));
},
onError: (error: Error) => {
setResponse(JSON.stringify({ error: error.message }, null, 2));
},
});

const onSubmit = async (formData: Record<string, any>) => {
const toolParams: Record<string, any> = {};
getParameters(tool).forEach((field) => {
if (field.name) {
let value = formData[field.name];
if (field.type === 'integer' || field.type === 'long') {
value = parseInt(value, 10);
} else if (field.type === 'double' || field.type === 'float') {
value = parseFloat(value);
}
toolParams[field.name] = value;
}
});

await executeTool({
toolId: tool!.id,
toolParams,
});
};

return (
<EuiFlyout onClose={onClose} aria-labelledby="flyoutTitle">
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<EuiTitle size="m">
<h2 id="flyoutTitle">
{i18n.translate('xpack.onechat.tools.testFlyout.title', {
defaultMessage: 'Test Tool',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<FormProvider {...form}>
<EuiFlexGroup gutterSize="l" responsive={false}>
<EuiFlexItem
grow={false}
css={css`
min-width: 200px;
max-width: 300px;
`}
>
<EuiTitle size="s">
<h5>
{i18n.translate('xpack.onechat.tools.testTool.inputsTitle', {
defaultMessage: 'Inputs',
})}
</h5>
</EuiTitle>
<EuiSpacer size="m" />
<EuiForm component="form" onSubmit={handleSubmit(onSubmit)}>
{getParameters(tool)?.map((field) => (
<EuiFormRow
key={field.name}
label={field.label}
isInvalid={!!errors[field.name]}
error={errors[field.name]?.message as string}
>
{field.type === 'integer' ||
field.type === 'long' ||
field.type === 'double' ||
field.type === 'float' ? (
<Controller
name={field.name}
control={form.control}
rules={{ required: `${field.label} is required` }}
render={({ field: { onChange, value, name } }) => (
<EuiFieldNumber
name={name}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={`Enter ${field.label.toLowerCase()}`}
fullWidth
/>
)}
/>
) : (
<Controller
name={field.name}
control={form.control}
rules={{ required: `${field.label} is required` }}
render={({ field: { onChange, value, name } }) => (
<EuiFieldText
name={name}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={`Enter ${field.label.toLowerCase()}`}
fullWidth
/>
)}
/>
)}
</EuiFormRow>
))}
<EuiSpacer size="m" />
<EuiButton type="submit" size="s" fill isLoading={isExecuting} disabled={!tool}>
{i18n.translate('xpack.onechat.tools.testTool.executeButton', {
defaultMessage: 'Submit',
})}
</EuiButton>
</EuiForm>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="s">
<h5>
{i18n.translate('xpack.onechat.tools.testTool.responseTitle', {
defaultMessage: 'Response',
})}
</h5>
</EuiTitle>
<EuiSpacer size="m" />
<EuiCodeBlock
language="json"
fontSize="s"
paddingSize="m"
isCopyable={true}
css={css`
height: 75vh;
overflow: auto;
`}
>
{response}
</EuiCodeBlock>
</EuiFlexItem>
</EuiFlexGroup>
</FormProvider>
</EuiFlyoutBody>
</EuiFlyout>
);
};
Loading