Skip to content

Commit e0efd3b

Browse files
feature: add export test cases (#2408)
1 parent 818556a commit e0efd3b

File tree

6 files changed

+64
-11
lines changed

6 files changed

+64
-11
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { cookies, headers } from 'next/headers';
2+
import { NextRequest } from 'next/server';
3+
4+
import { testSuitesApi } from '@/src/app/api/api';
5+
import { getUserToken } from '@/src/utils/auth/auth-request';
6+
import { getIsEnableAuthToggle } from '@/src/utils/env/get-auth-toggle';
7+
8+
export async function GET(req: NextRequest) {
9+
const token = await getUserToken(getIsEnableAuthToggle(), headers(), cookies());
10+
const reqUrl = req.url;
11+
const { searchParams } = new URL(reqUrl);
12+
const id = searchParams.get('id') as string;
13+
return testSuitesApi.exportTestCasesCsv(decodeURIComponent(id), token);
14+
}

apps/ai-dial-admin/src/components/Common/ComplexInput/ComplexInput.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ const ComplexInput: FC<Props> = ({ fullValue, label, isFullWidth, copyable = tru
2222
const t = useI18n();
2323
return (
2424
<div className="flex items-end gap-2">
25-
<DialInput
26-
labelProps={{ label, required }}
27-
containerClassName={mergeClasses(
28-
isFullWidth ? 'w-full' : copyable ? CONTROL_WITH_BUTTON_WIDTH : STANDARD_CONTROL_WIDTH,
29-
)}
30-
{...props}
31-
/>
32-
{copyable && <CopyButton valueLabel={label} value={fullValue} buttonLabel={t(ButtonsI18nKey.Copy)} />}
25+
<div className="flex items-end gap-2">
26+
<DialInput
27+
labelProps={{ label, required }}
28+
containerClassName={mergeClasses(
29+
isFullWidth ? 'w-full' : copyable ? CONTROL_WITH_BUTTON_WIDTH : STANDARD_CONTROL_WIDTH,
30+
)}
31+
{...props}
32+
/>
33+
{copyable && <CopyButton valueLabel={label} value={fullValue} buttonLabel={t(ButtonsI18nKey.Copy)} />}
34+
</div>
3335
</div>
3436
);
3537
};

apps/ai-dial-admin/src/components/TestSuites/TestCases/Header.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { FC, useMemo, useState } from 'react';
44
import { createPortal } from 'react-dom';
55

6-
import { IconPlus } from '@tabler/icons-react';
6+
import { IconDownload, IconPlus } from '@tabler/icons-react';
77
import {
88
ButtonAppearance,
99
ButtonVariant,
@@ -21,8 +21,9 @@ interface Props {
2121
selectedTestSuiteId: string;
2222
onApplyImport: (file: File) => void;
2323
onAdd?: () => void;
24+
onExport?: () => void;
2425
}
25-
const HeaderButtons: FC<Props> = ({ selectedTestSuiteId, onApplyImport, onAdd }) => {
26+
const HeaderButtons: FC<Props> = ({ selectedTestSuiteId, onApplyImport, onAdd, onExport }) => {
2627
const t = useI18n();
2728

2829
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
@@ -40,6 +41,15 @@ const HeaderButtons: FC<Props> = ({ selectedTestSuiteId, onApplyImport, onAdd })
4041
appearance={ButtonAppearance.Ghost}
4142
/>
4243

44+
{onExport && (
45+
<DialPrimaryButton
46+
label={t(ButtonsI18nKey.Export)}
47+
iconBefore={<IconDownload {...BASE_BUTTON_ICON_PROPS} />}
48+
onClick={onExport}
49+
appearance={ButtonAppearance.Ghost}
50+
/>
51+
)}
52+
4353
<DialPrimaryButton
4454
label={t(ButtonsI18nKey.Add)}
4555
iconBefore={<IconPlus {...BASE_BUTTON_ICON_PROPS} />}

apps/ai-dial-admin/src/components/TestSuites/TestCases/TestCasesList.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { SaveValidationContextProvider } from '@/src/context/SaveValidationConte
1919
import { useI18n } from '@/src/locales/client';
2020
import { TestCase, TestSuite } from '@/src/models/evaluation/test-suite';
2121
import { getErrorNotification, getSuccessNotification } from '@/src/utils/notification';
22-
import HeaderButtons from './Header';
2322
import { DialLoader } from '@epam/ai-dial-ui-kit';
23+
import HeaderButtons from './Header';
2424

2525
export interface TestCasesActions {
2626
getDirtyTestCases: () => TestCase[];
@@ -152,6 +152,13 @@ const TestCasesList: FC<Props> = ({ selectedTestSuite, testCasesActionsRef, onDi
152152
[refreshGrid, selectedTestSuite.id, showNotification, t],
153153
);
154154

155+
const onExport = useCallback(() => {
156+
const testSuiteId = selectedTestSuite.id;
157+
if (!testSuiteId) return;
158+
159+
window.open(`/api/test-suites/export?id=${encodeURIComponent(testSuiteId)}`, '_blank');
160+
}, [selectedTestSuite.id, showNotification, t]);
161+
155162
const onAddTestCase = useCallback(() => {
156163
const testSuiteId = selectedTestSuite.id;
157164
if (!testSuiteId) return;
@@ -244,6 +251,7 @@ const TestCasesList: FC<Props> = ({ selectedTestSuite, testCasesActionsRef, onDi
244251
selectedTestSuiteId={selectedTestSuite.id as string}
245252
onApplyImport={onApplyImport}
246253
onAdd={onAddTestCase}
254+
onExport={onExport}
247255
/>
248256
</ListEntities>
249257
)}

apps/ai-dial-admin/src/server/eval/test-suites-api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ export class TestSuitesApi extends BaseApi {
9797
return this.putAction(TEST_CASES_URL(id), testCases, token);
9898
}
9999

100+
exportTestCasesCsv(testSuiteId: string, token: Token) {
101+
const filename = `test_suite_${testSuiteId}_export.csv`;
102+
return this.streamRequest(`${TEST_CASES_URL(testSuiteId)}/export.csv`, filename, token);
103+
}
104+
100105
createTestSuite(suite: TestSuite, token: Token): Promise<ServerActionResponse> {
101106
return this.postAction(TEST_SUITES_URL, suite, token);
102107
}

apps/ai-dial-admin/src/server/eval/tests/test-suites-api.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,20 @@ describe('Server :: TestSuiteApi', () => {
243243
);
244244
});
245245

246+
test('Should call exportTestCasesCsv', async () => {
247+
const streamResponse = new Response(null, { status: 200 });
248+
const streamSpy = vi.spyOn(instance as any, 'streamRequest').mockResolvedValue(streamResponse);
249+
250+
const result = await instance.exportTestCasesCsv('suite-id', TOKEN_MOCK);
251+
252+
expect(streamSpy).toHaveBeenCalledWith(
253+
`${TEST_CASES_URL('suite-id')}/export.csv`,
254+
'test_suite_suite-id_export.csv',
255+
TOKEN_MOCK,
256+
);
257+
expect(result).toBe(streamResponse);
258+
});
259+
246260
test('Should call getTestSuiteTemplateVariables', async () => {
247261
fetch.mockResponseOnce(JSON.stringify(mockTestSuite));
248262
await instance.getTestSuiteTemplateVariables('id', TOKEN_MOCK);

0 commit comments

Comments
 (0)