Skip to content

Commit 7fbf1bb

Browse files
Merge branch 'development' into fix/file-inport
2 parents 0c9f83b + 818556a commit 7fbf1bb

File tree

8 files changed

+169
-28
lines changed

8 files changed

+169
-28
lines changed

apps/ai-dial-admin/src/app/[lang]/deployment-images/[id]/page.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { notFound } from 'next/navigation';
22

3-
import { getImage, getImageVersions } from '@/src/app/actions/deployments';
3+
import { getContainers, getImage, getImageVersions } from '@/src/app/actions/deployments';
44
import ImageView from '@/src/components/Images/View/ImageView';
55
import { SaveValidationContextProvider } from '@/src/context/SaveValidationContext';
66
import { Image, ImageVersion } from '@/src/models/deployments/images';
77
import { errorObjLog } from '@/src/server/logger';
88
import { getRouteByType } from '@/src/utils/deployments/entity';
99
import { getImageType } from '@/src/utils/deployments/images';
10+
import { Container } from '@/src/models/deployments/containers';
1011

1112
export const dynamic = 'force-dynamic';
1213

@@ -18,15 +19,16 @@ interface Params {
1819
export default async function Page(params: Params) {
1920
let image: Image | null = null;
2021
let versions: ImageVersion[] | null = null;
22+
let containers: Container[] | null = null;
2123

2224
try {
2325
const imageResponse = await getImage((await params.params).id);
24-
26+
const containersResponse = await getContainers();
2527
const versionsResponse = await getImageVersions(
2628
imageResponse.response.name,
2729
getImageType(getRouteByType(imageResponse.response.$type)),
2830
);
29-
31+
containers = containersResponse.response as Container[];
3032
image = imageResponse.response as Image;
3133
versions = versionsResponse.response as ImageVersion[];
3234
} catch (e) {
@@ -37,9 +39,11 @@ export default async function Page(params: Params) {
3739
notFound();
3840
}
3941

42+
const names = containers?.map((container) => container.name || '') || [];
43+
4044
return (
4145
<SaveValidationContextProvider>
42-
<ImageView image={image} versions={versions || []} />
46+
<ImageView image={image} versions={versions || []} containerNames={names} />
4347
</SaveValidationContextProvider>
4448
);
4549
}

apps/ai-dial-admin/src/components/Common/Lists/CheckboxList.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useCallback, useEffect, useState } from 'react';
1+
import { FC, useCallback, useEffect, useRef, useState } from 'react';
22
import NewItem from '@/src/components/Common/Multiselect/Modal/NewItem';
33
import { DialCheckbox, DialNeutralButton } from '@epam/ai-dial-ui-kit';
44
import { IconPlus } from '@tabler/icons-react';
@@ -23,6 +23,8 @@ const CheckboxList: FC<Props> = ({
2323
addTitle,
2424
addPlaceholder,
2525
}) => {
26+
const newItemsContainer = useRef<HTMLUListElement | null>(null);
27+
2628
const [list, setList] = useState<string[]>(items);
2729
const [newList, setNewList] = useState<string[]>([]);
2830

@@ -73,9 +75,21 @@ const CheckboxList: FC<Props> = ({
7375
setNewList((prev) => [...prev, '']);
7476
}, []);
7577

78+
useEffect(() => {
79+
const container = newItemsContainer.current;
80+
if (container && container.scrollHeight > container.clientHeight) {
81+
setTimeout(() => {
82+
container.scrollTo({
83+
top: container.scrollHeight,
84+
behavior: 'smooth',
85+
});
86+
});
87+
}
88+
}, [newList.length]);
89+
7690
return (
7791
<>
78-
<ul className="flex flex-col gap-y-2 overflow-auto flex-1 min-h-0">
92+
<ul className="flex flex-col gap-y-2 overflow-auto flex-1 min-h-0" ref={newItemsContainer}>
7993
{list.map((item, index) => (
8094
<li key={`item_${index}`}>
8195
<DialCheckbox

apps/ai-dial-admin/src/components/Containers/View/ExecutionLog/ExecutionLog.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { FC, useCallback, useEffect, useState } from 'react';
2-
import { DialCollapsibleSidebar, DialNoDataContent, DialTabs, TabModel, TabOrientation } from '@epam/ai-dial-ui-kit';
2+
import { DialCollapsibleSidebar, DialTabs, TabModel, TabOrientation } from '@epam/ai-dial-ui-kit';
33

44
import { Pod } from '@/src/models/deployments/containers';
5-
import { ContainersI18nKey, EntitiesI18nKey, EntityFieldsI18nKey } from '@/src/constants/i18n';
5+
import { ContainersI18nKey, EntityFieldsI18nKey } from '@/src/constants/i18n';
66
import { EntityViewTab } from '@/src/utils/tabs/utils';
77
import { ApplicationRoute } from '@/src/types/routes';
8-
import { getTranslatedDeploymentType } from '@/src/utils/deployments/entity';
8+
99
import { useI18n } from '@/src/locales/client';
1010

1111
import PodView from '@/src/components/Containers/View/ExecutionLog/PodView';
@@ -58,11 +58,7 @@ const ExecutionLog: FC<Props> = ({ containerId, route, pods }) => {
5858

5959
return (
6060
<div className="flex flex-col h-full w-full">
61-
{!tabs.length ? (
62-
<DialNoDataContent
63-
title={t(EntitiesI18nKey.NoContainerLogs, { entityType: getTranslatedDeploymentType(route, t) })}
64-
/>
65-
) : (
61+
{!!tabs.length && (
6662
<div className="flex h-full min-h-0 gap-8">
6763
{tabs.length > 1 && (
6864
<DialCollapsibleSidebar
@@ -80,7 +76,11 @@ const ExecutionLog: FC<Props> = ({ containerId, route, pods }) => {
8076
/>
8177
</DialCollapsibleSidebar>
8278
)}
83-
<PodView pod={pods.find((pod) => pod.name === activeTab) ?? pods[0]} containerId={containerId} />
79+
<PodView
80+
pod={pods.find((pod) => pod.name === activeTab) ?? pods[0]}
81+
containerId={containerId}
82+
route={route}
83+
/>
8484
</div>
8585
)}
8686
</div>

apps/ai-dial-admin/src/components/Containers/View/ExecutionLog/PodView.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { FC, useEffect, useState } from 'react';
2-
2+
import { DialNoDataContent } from '@epam/ai-dial-ui-kit';
33
import { Pod } from '@/src/models/deployments/containers';
4-
import { ErrorI18nKey, EntityFieldsI18nKey, DeploymentsI18nKey } from '@/src/constants/i18n';
4+
import { ErrorI18nKey, EntityFieldsI18nKey, DeploymentsI18nKey, EntitiesI18nKey } from '@/src/constants/i18n';
55
import { formatDateTimeToLocalString } from '@/src/utils/formatting/date';
66
import { RESTART_REASONS } from '@/src/constants/deployments/containers';
77
import { useI18n } from '@/src/locales/client';
88
import { useNotification } from '@/src/context/NotificationContext';
99
import { getErrorNotification } from '@/src/utils/notification';
10+
import { getTranslatedDeploymentType } from '@/src/utils/deployments/entity';
1011

1112
import LogViewer from '@/src/components/Common/LogViewer/LogViewer';
1213
import LabelledText from '@/src/components/Common/LabelledText/LabelledText';
14+
import { ApplicationRoute } from '@/src/types/routes';
1315

1416
interface Props {
1517
pod: Pod;
1618
containerId?: string;
19+
route: ApplicationRoute;
1720
}
1821

19-
const PodView: FC<Props> = ({ pod, containerId }) => {
22+
const PodView: FC<Props> = ({ pod, containerId, route }) => {
2023
const t = useI18n();
2124
const { showNotification } = useNotification();
2225
const [logs, setLogs] = useState('');
@@ -78,7 +81,13 @@ const PodView: FC<Props> = ({ pod, containerId }) => {
7881
</div>
7982
)}
8083
<div className="flex flex-1 min-h-0 h-full">
81-
<LogViewer logs={logs} />
84+
{logs.length ? (
85+
<LogViewer logs={logs} />
86+
) : (
87+
<DialNoDataContent
88+
title={t(EntitiesI18nKey.NoContainerLogs, { entityType: getTranslatedDeploymentType(route, t) })}
89+
/>
90+
)}
8291
</div>
8392
</div>
8493
);
Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
1-
import { describe, expect, test } from 'vitest';
2-
import { render, screen } from '@testing-library/react';
3-
import ExecutionLog from '../ExecutionLog';
4-
import { EntitiesI18nKey } from '@/src/constants/i18n';
1+
import { describe, expect, test, vi } from 'vitest';
2+
import { render } from '@testing-library/react';
53
import { ApplicationRoute } from '@/src/types/routes';
64

7-
describe('ExecutionLog', () => {
8-
test('renders with empty logs', () => {
9-
render(<ExecutionLog containerId="" pods={[]} route={ApplicationRoute.McpContainers} />);
5+
import ExecutionLog from '../ExecutionLog';
106

11-
expect(screen.getByText(EntitiesI18nKey.NoContainerLogs)).toBeInTheDocument();
7+
vi.mock('@epam/ai-dial-ui-kit', () => ({
8+
DialCollapsibleSidebar: ({ children, title }: any) => (
9+
<div role="complementary" aria-label={title}>
10+
{children}
11+
</div>
12+
),
13+
DialTabs: ({ tabs, activeTab, onClick }: any) => (
14+
<div role="tablist">
15+
{tabs.map((tab: any) => (
16+
<button key={tab.id} role="tab" aria-selected={tab.id === activeTab} onClick={() => onClick(tab.id)}>
17+
{tab.label}
18+
</button>
19+
))}
20+
</div>
21+
),
22+
TabOrientation: { Vertical: 'vertical' },
23+
}));
24+
25+
vi.mock('@/src/components/Containers/View/ExecutionLog/PodView', () => ({
26+
__esModule: true,
27+
default: ({ pod, containerId }: any) => (
28+
<div data-testid="pod-view" data-pod-name={pod?.name} data-container-id={containerId} />
29+
),
30+
}));
31+
32+
describe('ExecutionLog', () => {
33+
test('renders nothing when pods list is empty', () => {
34+
const { container } = render(<ExecutionLog containerId="c1" pods={[]} route={ApplicationRoute.McpContainers} />);
35+
expect(container.firstChild).toBeEmptyDOMElement();
1236
});
1337
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, expect, test, vi, beforeEach } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
4+
const mockAddEventListener = vi.fn();
5+
const mockRemoveEventListener = vi.fn();
6+
const mockClose = vi.fn();
7+
8+
class MockEventSource {
9+
constructor(public url: string) {
10+
MockEventSource.instances.push(this);
11+
}
12+
static instances: MockEventSource[] = [];
13+
addEventListener = mockAddEventListener;
14+
removeEventListener = mockRemoveEventListener;
15+
close = mockClose;
16+
}
17+
18+
vi.stubGlobal('EventSource', MockEventSource);
19+
20+
vi.mock('@epam/ai-dial-ui-kit', () => ({
21+
DialNoDataContent: ({ title }: any) => <div data-testid="no-data">{title}</div>,
22+
}));
23+
24+
vi.mock('@/src/components/Common/LogViewer/LogViewer', () => ({
25+
__esModule: true,
26+
default: ({ logs }: any) => <div data-testid="log-viewer">{logs}</div>,
27+
}));
28+
29+
vi.mock('@/src/components/Common/LabelledText/LabelledText', () => ({
30+
__esModule: true,
31+
default: ({ label, text }: any) => <div data-testid={`label-${label}`}>{text}</div>,
32+
}));
33+
34+
import PodView from '../PodView';
35+
import { EntityFieldsI18nKey } from '@/src/constants/i18n';
36+
import { ApplicationRoute } from '@/src/types/routes';
37+
import type { Pod } from '@/src/models/deployments/containers';
38+
39+
const makePod = (name: string, overrides?: Partial<Pod>): Pod => ({
40+
name,
41+
createdAt: Date.now(),
42+
...overrides,
43+
});
44+
45+
describe('PodView', () => {
46+
beforeEach(() => {
47+
vi.clearAllMocks();
48+
MockEventSource.instances = [];
49+
});
50+
51+
test('shows no-data placeholder when there are no logs', () => {
52+
render(<PodView pod={makePod('pod-1')} containerId="c1" route={ApplicationRoute.McpContainers} />);
53+
expect(screen.getByTestId('no-data')).toBeInTheDocument();
54+
});
55+
56+
test('opens EventSource with correct URL', () => {
57+
render(<PodView pod={makePod('pod-1')} containerId="c1" route={ApplicationRoute.McpContainers} />);
58+
expect(MockEventSource.instances).toHaveLength(1);
59+
expect(MockEventSource.instances[0].url).toBe('/api/sse?entity=container&id=c1&podName=pod-1');
60+
});
61+
62+
test('does not open EventSource when containerId is missing', () => {
63+
render(<PodView pod={makePod('pod-1')} route={ApplicationRoute.McpContainers} />);
64+
expect(MockEventSource.instances).toHaveLength(0);
65+
});
66+
67+
test('displays restart info when pod has restarts', () => {
68+
render(
69+
<PodView
70+
pod={makePod('pod-1', { restartCount: 5, lastFinishedAt: 1700000000000, lastTerminationReason: 'OOMKilled' })}
71+
containerId="c1"
72+
route={ApplicationRoute.McpContainers}
73+
/>,
74+
);
75+
expect(screen.getByTestId(`label-${EntityFieldsI18nKey.Restarts}`)).toHaveTextContent('5');
76+
expect(screen.getByTestId(`label-${EntityFieldsI18nKey.LastRestartedAt}`)).toBeInTheDocument();
77+
expect(screen.getByTestId(`label-${EntityFieldsI18nKey.LastReason}`)).toBeInTheDocument();
78+
});
79+
80+
test('hides restart info when restartCount is absent', () => {
81+
render(<PodView pod={makePod('pod-1')} containerId="c1" route={ApplicationRoute.McpContainers} />);
82+
expect(screen.queryByTestId(`label-${EntityFieldsI18nKey.Restarts}`)).not.toBeInTheDocument();
83+
});
84+
});

apps/ai-dial-admin/src/components/Containers/View/Prompts/Prompts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Prompts: FC<Props> = ({ containerId }) => {
3737

3838
return (
3939
<div className="flex flex-col">
40-
<div className="flex flex-col mt-6">
40+
<div className="flex flex-col gap-6">
4141
{prompts?.map((prompt, index) => {
4242
return <PromptComponent prompt={prompt} key={index} />;
4343
})}

apps/ai-dial-admin/src/constants/file.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export const contentTypes: Record<string, string> = {
5757
'.csv': 'text/csv',
5858
// pdf
5959
'.pdf': 'application/pdf',
60+
// audio/video
61+
'.mp3': 'audio/mpeg',
62+
'.wav': 'audio/wav',
63+
'.ogg': 'audio/ogg',
64+
'.mp4': 'video/mp4',
65+
'.webm': 'video/webm',
6066
};
6167
// same as bodySizeLimit in server config
6268
export const MAX_FILE_SIZE_MB = 4;

0 commit comments

Comments
 (0)