Skip to content

Commit 6574917

Browse files
feature: App Routes - add permission and attachments paths (Issue #308) (#440)
1 parent 06d7ca0 commit 6574917

File tree

12 files changed

+206
-104
lines changed

12 files changed

+206
-104
lines changed

apps/ai-dial-admin/src/components/EntityView/AppRoute/AppRoute.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import Tabs from '@/src/components/Common/Tabs/Tabs';
66
import { ButtonsI18nKey, TabsI18nKey } from '@/src/constants/i18n';
77
import { BASE_ICON_PROPS } from '@/src/constants/main-layout';
88
import { useI18n } from '@/src/locales/client';
9-
import { DialRoute } from '@/src/models/dial/route';
9+
import { DialAppRoute } from '@/src/models/dial/route';
1010
import { TabModel } from '@/src/models/tab';
1111
import { PopUpState } from '@/src/types/pop-up';
1212
import { TabOrientation } from '@/src/types/tab';
13-
import CreateRoute from './CreateRoute';
14-
import RouteContent from './RouteContent';
13+
import CreateRoute from '@/src/components/EntityView/AppRoute/CreateRoute';
14+
import RouteContent from '@/src/components/EntityView/AppRoute/Content/RouteContent';
1515

1616
interface Props {
17-
routes?: DialRoute[];
18-
onChangeRoutes: (routes: DialRoute[]) => void;
17+
routes?: DialAppRoute[];
18+
onChangeRoutes: (routes: DialAppRoute[]) => void;
1919
}
2020

2121
const EntityRoutes: FC<Props> = ({ routes, onChangeRoutes }) => {
@@ -49,7 +49,7 @@ const EntityRoutes: FC<Props> = ({ routes, onChangeRoutes }) => {
4949
}, []);
5050

5151
const onChangeRoute = useCallback(
52-
(route: DialRoute) => {
52+
(route: DialAppRoute) => {
5353
if (routes) {
5454
routes[activeRouteIndex as number] = route;
5555
onChangeRoutes([...(routes || [])]);
@@ -61,7 +61,7 @@ const EntityRoutes: FC<Props> = ({ routes, onChangeRoutes }) => {
6161
const onCreate = useCallback(
6262
(name: string) => {
6363
handleModalClose();
64-
onChangeRoutes([...(routes || []), { name } as DialRoute]);
64+
onChangeRoutes([...(routes || []), { name } as DialAppRoute]);
6565
},
6666
[handleModalClose, onChangeRoutes, routes],
6767
);
@@ -93,7 +93,7 @@ const EntityRoutes: FC<Props> = ({ routes, onChangeRoutes }) => {
9393
<div className="flex flex-col flex-1 min-h-0 min-w-0 relative">
9494
{routes?.[activeRouteIndex as number] && (
9595
<RouteContent
96-
route={routes?.[activeRouteIndex as number] || ({} as DialRoute)}
96+
route={routes?.[activeRouteIndex as number] || ({} as DialAppRoute)}
9797
onChangeRoute={onChangeRoute}
9898
/>
9999
)}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use client';
2+
import { FC, useCallback } from 'react';
3+
4+
import Paths from '@/src/components/Routes/Paths/Paths';
5+
import { RoutesI18nKey } from '@/src/constants/i18n';
6+
import { useI18n } from '@/src/locales/client';
7+
import { AttachmentPaths, DialAppRoute } from '@/src/models/dial/route';
8+
9+
interface Props {
10+
route: DialAppRoute;
11+
onChangeRoute: (route: DialAppRoute) => void;
12+
}
13+
14+
const RouteAttachments: FC<Props> = ({ route, onChangeRoute }) => {
15+
const t = useI18n() as (str: string) => string;
16+
17+
const onChangeRequest = useCallback(
18+
(paths: string[]) => {
19+
onChangeRoute({
20+
...route,
21+
attachmentPaths: { ...(route.attachmentPaths || {}), requestBody: paths } as AttachmentPaths,
22+
});
23+
},
24+
[route, onChangeRoute],
25+
);
26+
27+
const onChangeResponse = useCallback(
28+
(paths: string[]) => {
29+
onChangeRoute({
30+
...route,
31+
attachmentPaths: { ...(route.attachmentPaths || {}), responseBody: paths } as AttachmentPaths,
32+
});
33+
},
34+
[route, onChangeRoute],
35+
);
36+
37+
return (
38+
<div className="h-full w-full flex flex-col divide-y gap-y-9 divide-primary">
39+
<div className="w-full lg:w-[50%]">
40+
<Paths
41+
title={t(RoutesI18nKey.RequestAttachmentPaths)}
42+
paths={route.attachmentPaths?.requestBody}
43+
onChangePaths={onChangeRequest}
44+
/>
45+
</div>
46+
<div className="w-full lg:w-[50%] pt-9">
47+
<Paths
48+
title={t(RoutesI18nKey.ResponseAttachmentPaths)}
49+
paths={route.attachmentPaths?.responseBody}
50+
onChangePaths={onChangeResponse}
51+
/>
52+
</div>
53+
</div>
54+
);
55+
};
56+
57+
export default RouteAttachments;

apps/ai-dial-admin/src/components/EntityView/AppRoute/RouteContent.tsx renamed to apps/ai-dial-admin/src/components/EntityView/AppRoute/Content/RouteContent.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
import { FC, useState } from 'react';
33

44
import Tabs from '@/src/components/Common/Tabs/Tabs';
5-
import { EntityViewTab, propertiesTabs } from '@/src/components/EntityView/View/utils';
5+
import { attachmentsTabs, EntityViewTab, propertiesTabs } from '@/src/components/EntityView/View/utils';
66
import { useI18n } from '@/src/locales/client';
7-
import { DialRoute } from '@/src/models/dial/route';
7+
import { DialAppRoute } from '@/src/models/dial/route';
88
import RouteProperties from '@/src/components/Routes/Properties/RouteProperties';
9+
import RouteAttachments from './RouteAttachments';
910

1011
interface Props {
11-
route: DialRoute;
12-
onChangeRoute: (route: DialRoute) => void;
12+
route: DialAppRoute;
13+
onChangeRoute: (route: DialAppRoute) => void;
1314
}
1415

1516
const RouteContent: FC<Props> = ({ route, onChangeRoute }) => {
1617
const t = useI18n() as (stringToTranslate: string) => string;
1718

18-
const tabs = [propertiesTabs(t)];
19+
const tabs = [propertiesTabs(t), attachmentsTabs(t)];
1920

2021
const [activeTab, setActiveTab] = useState(EntityViewTab.Properties);
2122

@@ -29,6 +30,8 @@ const RouteContent: FC<Props> = ({ route, onChangeRoute }) => {
2930
{activeTab === EntityViewTab.Properties && (
3031
<RouteProperties route={route} updateRoute={onChangeRoute} isAppRoute={true} />
3132
)}
33+
34+
{activeTab === EntityViewTab.Attachments && <RouteAttachments route={route} onChangeRoute={onChangeRoute} />}
3235
</div>
3336
</div>
3437
);

apps/ai-dial-admin/src/components/EntityView/View/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TabsI18nKey } from '@/src/constants/i18n';
22
import { TabModel } from '@/src/models/tab';
33
import { ApplicationRoute } from '@/src/types/routes';
4-
import { DialApplication, DialApplicationScheme } from '../../../models/dial/application';
4+
import { DialApplication, DialApplicationScheme } from '@/src/models/dial/application';
55

66
export enum EntityViewTab {
77
Properties = 'Properties',
@@ -20,6 +20,7 @@ export enum EntityViewTab {
2020
Routes = 'Routes',
2121
Traces = 'Traces',
2222
Conversations = 'Conversations',
23+
Attachments = 'Attachments',
2324
}
2425

2526
export const propertiesTabs = (t: (stringToTranslate: string) => string) => ({
@@ -87,6 +88,11 @@ export const conversationsTabs = (t: (stringToTranslate: string) => string) => (
8788
name: t(TabsI18nKey.Conversations),
8889
});
8990

91+
export const attachmentsTabs = (t: (stringToTranslate: string) => string) => ({
92+
id: EntityViewTab.Attachments,
93+
name: t(TabsI18nKey.Attachments),
94+
});
95+
9096
export const getViewTabs = (
9197
t: (stringToTranslate: string) => string,
9298
view: ApplicationRoute,

apps/ai-dial-admin/src/components/MaxRetryAttempts/MaxRetryAttempts.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,12 @@ const MaxRetryAttempts: FC<Props> = ({ maxRetryAttempts, onChangeMaxRetryAttempt
1414
const t = useI18n();
1515

1616
const items: DropdownItemsModel[] = [
17-
{
18-
id: '0',
19-
name: t(BasicI18nKey.None),
20-
},
21-
{
22-
id: '1',
23-
name: '1',
24-
},
25-
{
26-
id: '2',
27-
name: '2',
28-
},
29-
{
30-
id: '3',
31-
name: '3',
32-
},
33-
{
34-
id: '4',
35-
name: '4',
36-
},
37-
{
38-
id: '5',
39-
name: '5',
40-
},
17+
{ id: '0', name: t(BasicI18nKey.None) },
18+
{ id: '1', name: '1' },
19+
{ id: '2', name: '2' },
20+
{ id: '3', name: '3' },
21+
{ id: '4', name: '4' },
22+
{ id: '5', name: '5' },
4123
];
4224
const activeMaxAttempts = maxRetryAttempts?.toString() || '0';
4325
const onChange = useCallback(

apps/ai-dial-admin/src/components/Routes/Paths/Path.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
1-
import { FC, useEffect, useState } from 'react';
1+
import { FC, useEffect, useMemo, useState } from 'react';
22

33
import { IconTrash } from '@tabler/icons-react';
44
import classNames from 'classnames';
55

66
import { TextInputField } from '@/src/components/Common/InputField/InputField';
7-
import { EntityFieldsI18nKey, EntityPlaceholdersI18nKey, ErrorI18nKey } from '@/src/constants/i18n';
7+
import { EntityPlaceholdersI18nKey, ErrorI18nKey } from '@/src/constants/i18n';
88
import { BASE_ICON_PROPS } from '@/src/constants/main-layout';
99
import { useI18n } from '@/src/locales/client';
1010
import { isValidRoutePath } from '@/src/utils/validation/path-error';
1111

1212
interface Props {
1313
index: number;
14+
fieldTitle: string;
1415
path: string;
1516
allPaths?: string[];
1617
onRemove: (index: number) => void;
1718
onChangePath: (index: number, value: string) => void;
1819
}
1920

20-
const Path: FC<Props> = ({ index, path, allPaths, onRemove, onChangePath }) => {
21+
const Path: FC<Props> = ({ index, path, fieldTitle, allPaths, onRemove, onChangePath }) => {
2122
const t = useI18n();
2223

2324
const [isEmptyPath, setIsEmptyPath] = useState(true);
2425
const [isInvalidPath, setIsInvalidPath] = useState(false);
2526
const isAllEmptyValues = !allPaths?.some((v) => v !== '');
27+
const error = useMemo(() => {
28+
return isEmptyPath && index === 0 && isAllEmptyValues
29+
? t(ErrorI18nKey.RequiredProperty)
30+
: isInvalidPath
31+
? t(ErrorI18nKey.InvalidPath)
32+
: '';
33+
}, [index, isAllEmptyValues, isEmptyPath, isInvalidPath, t]);
2634

2735
const removeButtonClass = classNames(
2836
'cursor-pointer ml-[10px]',
@@ -48,15 +56,9 @@ const Path: FC<Props> = ({ index, path, allPaths, onRemove, onChangePath }) => {
4856
elementId={'path ' + index}
4957
value={path}
5058
placeholder={t(EntityPlaceholdersI18nKey.PathUrl)}
51-
fieldTitle={index === 0 ? t(EntityFieldsI18nKey.paths) : ''}
59+
fieldTitle={index === 0 ? fieldTitle : ''}
5260
onChange={(value) => onChangePath(index, value)}
53-
errorText={
54-
isEmptyPath && index === 0 && isAllEmptyValues
55-
? t(ErrorI18nKey.RequiredProperty)
56-
: isInvalidPath
57-
? t(ErrorI18nKey.InvalidPath)
58-
: ''
59-
}
61+
errorText={error}
6062
invalid={(isEmptyPath && index === 0 && isAllEmptyValues) || isInvalidPath}
6163
/>
6264
</div>

apps/ai-dial-admin/src/components/Routes/Paths/Paths.spec.tsx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,51 @@ import { EntityFieldsI18nKey, EntityPlaceholdersI18nKey, RoutesI18nKey } from '@
55

66
describe('Paths', () => {
77
test('renders empty path input if no paths', () => {
8-
render(<Paths route={{ paths: [] }} updateRoute={vi.fn()} />);
8+
render(<Paths paths={[]} title="title" onChangePaths={vi.fn()} />);
99
expect(screen.getByPlaceholderText(EntityPlaceholdersI18nKey.PathUrl)).toBeInTheDocument();
10-
expect(screen.getByText(EntityFieldsI18nKey.paths)).toBeInTheDocument();
10+
expect(screen.getByText('title')).toBeInTheDocument();
1111
});
1212

1313
test('renders Path components for each path', () => {
14-
render(<Paths route={{ paths: ['/a', '/b'] }} updateRoute={vi.fn()} />);
14+
render(<Paths paths={['/a', '/b']} title="title" onChangePaths={vi.fn()} />);
1515
expect(screen.getByDisplayValue('/a')).toBeInTheDocument();
1616
expect(screen.getByDisplayValue('/b')).toBeInTheDocument();
1717
});
1818

19-
test('calls updateRoute when AddPaths is clicked', () => {
20-
const updateRoute = vi.fn();
21-
render(<Paths route={{ paths: ['/a'] }} updateRoute={updateRoute} />);
22-
fireEvent.click(screen.getByText(RoutesI18nKey.AddPaths));
23-
expect(updateRoute).toHaveBeenCalledWith({ paths: ['/a', ''] });
19+
test('calls onChangePaths when AddPaths is clicked', () => {
20+
const onChangePaths = vi.fn();
21+
render(<Paths paths={['/a']} title="title" onChangePaths={onChangePaths} />);
22+
fireEvent.click(screen.getByRole('button', { name: RoutesI18nKey.AddPaths }));
23+
expect(onChangePaths).toHaveBeenCalledWith(['/a', '']);
2424
});
2525

26-
test('calls updateRoute with two empty paths if adding first path', () => {
27-
const updateRoute = vi.fn();
28-
render(<Paths route={{ paths: void 0 }} updateRoute={updateRoute} />);
26+
test('calls onChangePaths with two empty paths if adding first path', () => {
27+
const onChangePaths = vi.fn();
28+
render(<Paths title="title" onChangePaths={onChangePaths} />);
2929
fireEvent.click(screen.getByText(RoutesI18nKey.AddPaths));
30-
expect(updateRoute).toHaveBeenCalledWith({ paths: ['', ''] });
30+
expect(onChangePaths).toHaveBeenCalledWith(['', '']);
3131
fireEvent.click(screen.getByLabelText('button'));
32-
expect(updateRoute).toHaveBeenCalledWith({ paths: ['', ''] });
32+
expect(onChangePaths).toHaveBeenCalledWith(['', '']);
3333
});
3434

35-
test('calls updateRoute when path input changes', () => {
36-
const updateRoute = vi.fn();
37-
render(<Paths route={{ paths: ['/a'] }} updateRoute={updateRoute} />);
35+
test('calls onChangePaths when path input changes', () => {
36+
const onChangePaths = vi.fn();
37+
render(<Paths paths={['/a']} title="title" onChangePaths={onChangePaths} />);
3838
fireEvent.change(screen.getByDisplayValue('/a'), { target: { value: '/changed' } });
39-
expect(updateRoute).toHaveBeenCalledWith({ paths: ['/changed'] });
39+
expect(onChangePaths).toHaveBeenCalledWith(['/changed']);
4040
});
4141

42-
test('calls updateRoute when Remove is clicked', () => {
43-
const updateRoute = vi.fn();
44-
render(<Paths route={{ paths: ['/a', '/b'] }} updateRoute={updateRoute} />);
42+
test('calls onChangePaths when Remove is clicked', () => {
43+
const onChangePaths = vi.fn();
44+
render(<Paths paths={['/a', '/b']} title="title" onChangePaths={onChangePaths} />);
4545
fireEvent.click(screen.getAllByLabelText('button')[0]);
46-
expect(updateRoute).toHaveBeenCalledWith({ paths: ['/b'] });
46+
expect(onChangePaths).toHaveBeenCalledWith(['/b']);
4747
});
4848

4949
test('clears path if only one and Remove is clicked', () => {
50-
const updateRoute = vi.fn();
51-
render(<Paths route={{ paths: ['/a'] }} updateRoute={updateRoute} />);
50+
const onChangePaths = vi.fn();
51+
render(<Paths paths={['/a']} title="title" onChangePaths={onChangePaths} />);
5252
fireEvent.click(screen.getByLabelText('button'));
53-
expect(updateRoute).toHaveBeenCalledWith({ paths: [''] });
53+
expect(onChangePaths).toHaveBeenCalledWith(['']);
5454
});
5555
});

0 commit comments

Comments
 (0)