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
32 changes: 32 additions & 0 deletions packages/shared/lib/api/sqle/service/audit_whitelist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,35 @@ export interface IUpdateAuditWhitelistByIdV1Params
}

export interface IUpdateAuditWhitelistByIdV1Return extends IBaseRes {}

export interface IGetAuditWhitelistExportsV1Params {
project_name: string;

filter_type?: getAuditWhitelistV1FilterTypeEnum;

fuzzy_search_content?: string;

filter_db_type?: string;

filter_rule_scope_mode?: getAuditWhitelistV1FilterRuleScopeModeEnum;

filter_rule_name?: string;

filter_audit_task_type?: string;

filter_audit_task_id?: string;

filter_sql_source?: getAuditWhitelistV1FilterSqlSourceEnum;

filter_created_by?: string;

filter_created_at_from?: string;

filter_created_at_to?: string;
}

export interface IGetAuditWhitelistExportsV1Return {
code?: number;

message?: string;
}
19 changes: 18 additions & 1 deletion packages/shared/lib/api/sqle/service/audit_whitelist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
IDeleteAuditWhitelistByIdV1Params,
IDeleteAuditWhitelistByIdV1Return,
IUpdateAuditWhitelistByIdV1Params,
IUpdateAuditWhitelistByIdV1Return
IUpdateAuditWhitelistByIdV1Return,
IGetAuditWhitelistExportsV1Params,
IGetAuditWhitelistExportsV1Return
} from './index.d';

class AuditWhitelistService extends ServiceBase {
Expand Down Expand Up @@ -103,6 +105,21 @@ class AuditWhitelistService extends ServiceBase {
options
);
}

public getAuditWhitelistExportsV1(
params: IGetAuditWhitelistExportsV1Params,
options?: AxiosRequestConfig
) {
const paramsData = this.cloneDeep(params);
const project_name = paramsData.project_name;
delete paramsData.project_name;

return this.get<IGetAuditWhitelistExportsV1Return>(
`/v1/projects/${project_name}/audit_whitelist/exports`,
paramsData,
options
);
}
}

export default new AuditWhitelistService();
3 changes: 3 additions & 0 deletions packages/sqle/src/locale/en-US/whitelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export default {

operate: {
addWhitelist: 'Add audit whitelist',
export: 'Export',
exporting: 'Exporting audit whitelist…',
exportSuccess: 'Audit whitelist exported successfully',

deleting: 'Deleting audit whitelist item...',
deleteSuccess: 'Successfully deleted audit whitelist item',
Expand Down
3 changes: 3 additions & 0 deletions packages/sqle/src/locale/zh-CN/whitelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export default {

operate: {
addWhitelist: '添加审核SQL例外',
export: '导出',
exporting: '正在导出审核SQL例外…',
exportSuccess: '审核SQL例外导出成功',

deleting: '正在删除审核SQL例外语句...',
deleteSuccess: '删除审核SQL例外语句成功',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ exports[`slqe/Whitelist/WhitelistList should render whitelist list 1`] = `
<div
class="extra"
>
<button
class="ant-btn css-dev-only-do-not-override-674gwq ant-btn-default basic-button-wrapper css-geipcv"
type="button"
>
<span
class="ant-btn-icon"
>
<svg
fill="currentColor"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.75 11.083h10.5v1.167H1.75zm5.833-3.4 3.542-3.541.825.824L7 9.916 2.05 4.968l.825-.825 3.542 3.54V1.167h1.166z"
/>
</svg>
</span>
<span>
导出
</span>
</button>
<button
class="ant-btn css-dev-only-do-not-override-674gwq ant-btn-primary basic-button-wrapper css-geipcv"
type="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
AuditWhitelistListFilterValues,
buildAuditWhitelistExportParams,
buildAuditWhitelistListParams
} from './buildAuditWhitelistListParams';
import { getAuditWhitelistV1FilterSqlSourceEnum } from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.enum';

describe('buildAuditWhitelistListParams / export', () => {
const filters: AuditWhitelistListFilterValues = {
filter_type: 'sql',
filter_db_type_value: 'MySQL',
filter_sql_source_value:
getAuditWhitelistV1FilterSqlSourceEnum.sql_audit_record,
filter_rule_scope_mode: 'specific',
filter_rule_name: 'rule_a',
filter_audit_task_type: 'sql_audit_record',
filter_audit_task_id: 'task-1',
filter_created_by: 'admin',
filter_created_at_from: '2026-01-01 00:00:00',
filter_created_at_to: '2026-01-31 23:59:59'
};

it('should map list filters with pagination', () => {
expect(
buildAuditWhitelistListParams({
projectName: 'demo',
pageIndex: 2,
pageSize: 20,
globalSearchKeyword: ' select ',
filters
})
).toEqual({
project_name: 'demo',
page_index: '2',
page_size: '20',
fuzzy_search_content: 'select',
filter_type: 'sql',
filter_db_type: 'MySQL',
filter_sql_source: 'sql_audit_record',
filter_rule_scope_mode: 'specific',
filter_rule_name: 'rule_a',
filter_audit_task_type: 'sql_audit_record',
filter_audit_task_id: 'task-1',
filter_created_by: 'admin',
filter_created_at_from: '2026-01-01 00:00:00',
filter_created_at_to: '2026-01-31 23:59:59'
});
});

it('should build export params with same filters and without pagination', () => {
const exportParams = buildAuditWhitelistExportParams({
projectName: 'demo',
globalSearchKeyword: ' select ',
filters
});

expect(exportParams).toEqual({
project_name: 'demo',
fuzzy_search_content: 'select',
filter_type: 'sql',
filter_db_type: 'MySQL',
filter_sql_source: 'sql_audit_record',
filter_rule_scope_mode: 'specific',
filter_rule_name: 'rule_a',
filter_audit_task_type: 'sql_audit_record',
filter_audit_task_id: 'task-1',
filter_created_by: 'admin',
filter_created_at_from: '2026-01-01 00:00:00',
filter_created_at_to: '2026-01-31 23:59:59'
});
expect(exportParams).not.toHaveProperty('page_index');
expect(exportParams).not.toHaveProperty('page_size');
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { IGetAuditWhitelistV1Params } from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.d';
import {
IGetAuditWhitelistExportsV1Params,
IGetAuditWhitelistV1Params
} from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.d';
import {
getAuditWhitelistV1FilterRuleScopeModeEnum,
getAuditWhitelistV1FilterSqlSourceEnum
Expand All @@ -25,6 +28,12 @@ type BuildAuditWhitelistListParamsOptions = {
filters: AuditWhitelistListFilterValues;
};

type BuildAuditWhitelistExportParamsOptions = {
projectName: string;
globalSearchKeyword?: string;
filters: AuditWhitelistListFilterValues;
};

const trimValue = (value?: string) => value?.trim() || undefined;

export const buildAuditWhitelistListParams = ({
Expand Down Expand Up @@ -89,3 +98,23 @@ export const buildAuditWhitelistListParams = ({

return params;
};

export const buildAuditWhitelistExportParams = ({
projectName,
globalSearchKeyword,
filters
}: BuildAuditWhitelistExportParamsOptions): IGetAuditWhitelistExportsV1Params => {
const listParams = buildAuditWhitelistListParams({
projectName,
pageIndex: 0,
pageSize: 0,
globalSearchKeyword,
filters
});
const {
page_index: _pageIndex,
page_size: _pageSize,
...exportParams
} = listParams;
return exportParams;
};
97 changes: 96 additions & 1 deletion packages/sqle/src/page/Whitelist/List/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { ModalName } from '../../../data/ModalName';
import { mockUseCurrentProject } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentProject';
import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser';
import { mockUseDbServiceDriver } from '@actiontech/shared/lib/testUtil/mockHook/mockUseDbServiceDriver';
import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi';
import {
createSpyFailResponse,
createSpySuccessResponse
} from '@actiontech/shared/lib/testUtil/mockApi';
import configuration from '../../../testUtils/mockApi/configuration';
import ruleTemplate from '../../../testUtils/mockApi/rule_template';
import {
Expand Down Expand Up @@ -79,6 +82,7 @@ describe('slqe/Whitelist/WhitelistList', () => {
await act(async () => jest.advanceTimersByTime(3000));
expect(baseElement).toMatchSnapshot();
expect(whitelistSpy).toHaveBeenCalledTimes(1);
expect(screen.getByText('导出')).toBeInTheDocument();
expect(screen.getByText('添加审核SQL例外')).toBeInTheDocument();
expect(
getBySelector('.custom-icon-refresh', baseElement)
Expand All @@ -91,6 +95,97 @@ describe('slqe/Whitelist/WhitelistList', () => {
);
});

it('should show export button for project member without action permission', async () => {
useCurrentUserSpy.mockImplementation(() => ({
...mockCurrentUserReturn,
isAdmin: false
}));
superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
expect(screen.getByText('导出')).toBeInTheDocument();
expect(screen.queryByText('添加审核SQL例外')).not.toBeInTheDocument();
});

it('should hide export button when current project is not accessible', async () => {
useCurrentProjectSpy.mockImplementation(() => ({
...mockProjectInfo,
projectName: ''
}));
superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
expect(screen.queryByText('导出')).not.toBeInTheDocument();
});

it('should hide export button when whitelist list request is forbidden', async () => {
whitelistSpy.mockImplementation(() =>
Promise.reject(new Error('Request failed with status code 403'))
);
superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
expect(screen.queryByText('导出')).not.toBeInTheDocument();
});

it('should export data file when click export button', async () => {
const exportSpy = auditWhiteList.exportAuditWhitelist();
superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
fireEvent.click(screen.getByText('导出'));
await act(async () => jest.advanceTimersByTime(300));
expect(screen.getByText('正在导出审核SQL例外…')).toBeInTheDocument();
expect(exportSpy).toHaveBeenCalledTimes(1);
expect(exportSpy).toHaveBeenCalledWith(
{
project_name: mockProjectInfo.projectName
},
{
responseType: 'blob'
}
);
expect(screen.getByText('导出').closest('button')).toBeDisabled();
await act(async () => jest.advanceTimersByTime(3300));
expect(screen.getByText('审核SQL例外导出成功')).toBeInTheDocument();
expect(screen.getByText('导出').closest('button')).not.toBeDisabled();
});

it('should prevent duplicate export requests while exporting', async () => {
const exportSpy = auditWhiteList.exportAuditWhitelist();
let resolveExport: (value: unknown) => void = () => undefined;
exportSpy.mockImplementation(
() =>
new Promise((resolve) => {
resolveExport = resolve;
}) as never
);
superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
fireEvent.click(screen.getByText('导出'));
await act(async () => jest.advanceTimersByTime(300));
expect(screen.getByText('导出').closest('button')).toBeDisabled();
fireEvent.click(screen.getByText('导出'));
expect(exportSpy).toHaveBeenCalledTimes(1);
await act(async () => {
resolveExport(createSpySuccessResponse({}));
});
await act(async () => jest.advanceTimersByTime(3000));
});

it('should restore export button when export fails', async () => {
const exportSpy = auditWhiteList.exportAuditWhitelist();
exportSpy.mockImplementation(() =>
createSpyFailResponse({
message: '导出条数超过上限,请缩小筛选条件后重试'
})
);
superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
fireEvent.click(screen.getByText('导出'));
await act(async () => jest.advanceTimersByTime(300));
expect(screen.getByText('导出').closest('button')).toBeDisabled();
await act(async () => jest.advanceTimersByTime(3000));
expect(screen.queryByText('审核SQL例外导出成功')).not.toBeInTheDocument();
expect(screen.getByText('导出').closest('button')).not.toBeDisabled();
});

test('refresh whitelist list', async () => {
const { baseElement } = superRender(<WhitelistList />);
await act(async () => jest.advanceTimersByTime(3000));
Expand Down
Loading
Loading