Skip to content
Open
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
3 changes: 2 additions & 1 deletion chat2db-community-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build": "yarn run build:web:community",
"build:prod": "yarn run build:web:community",
"build:web": "umi build",
"prebuild:web:community": "yarn test:community-boundary && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:console-tab-name && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor",
"prebuild:web:community": "yarn test:community-boundary && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:console-tab-name && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor && yarn test:operation-log-filters",
"postbuild:web:community": "node ./scripts/verify-production-bundles.cjs",
"build:web:2java": "yarn run build:web:community && rm -rf ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/* && cp -r dist/index.html ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/",
"build:web:community": "cross-env UMI_ENV=community cross-env APP_NAME=chat2db-community cross-env APP_VERSION=${npm_config_app_version} cross-env PRINT_LOGS=${npm_config_print_logs} cross-env APP_PORT=${npm_config_app_port} umi build",
Expand Down Expand Up @@ -61,6 +61,7 @@
"test:settings-layout": "tsx src/blocks/Setting/navigation.test.ts && tsx src/blocks/Setting/search.test.ts && tsx src/blocks/Setting/BaseSetting/model.test.ts && tsx src/client-extension/settingMenus.test.ts && tsx src/blocks/Setting/settingsLayout.test.ts",
"test:shortcut": "tsx src/constants/shortcut.test.ts && tsx src/utils/appTitleBarAction.test.ts && tsx src/layouts/GlobalLayout/AppTitleBar/platform.test.ts && tsx src/utils/jcefZoom.test.ts",
"test:sql-execution-log": "tsx src/service/sqlExecutionLog.test.ts",
"test:operation-log-filters": "tsx src/components/OperationLogFilters/model.test.ts && tsx src/hooks/selectDatabaseRequestLifecycle.test.ts",
"test:sql-execution-batch": "tsx src/service/sqlExecutionBatch.test.ts",
"test:data-source-execution-snapshot": "tsx src/service/dataSourceExecutionSnapshot.test.ts",
"test:sql-execution-request-tracker": "tsx src/service/sqlExecutionRequestTracker.test.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import i18n from '@/i18n';
import useSelectDatabase from '@/hooks/useSelectDatabase';
import { Input, Select } from 'antd';
import classnames from 'classnames';
import { memo } from 'react';
import { OperationLogFilterValues, updateOperationLogFilters } from './model';
import { useStyles } from './style';

interface IProps {
className?: string;
value: OperationLogFilterValues;
onChange: (value: OperationLogFilterValues) => void;
size?: 'small' | 'middle' | 'large';
}

function OperationLogFilters({ className, value, onChange, size = 'middle' }: IProps) {
const { styles } = useStyles();
const { dataSourceList, databaseList, schemaList, selectDatabase, onChangeSelectDatabase } = useSelectDatabase({});
const hasDataSource = value.dataSourceId !== undefined;
const databaseEnabled = hasDataSource && selectDatabase?.supportDatabase !== false;
const schemaEnabled =
hasDataSource &&
selectDatabase?.supportSchema !== false &&
(selectDatabase?.supportDatabase === false || !!value.databaseName);

const handleDataSourceChange = (dataSourceId?: number) => {
onChangeSelectDatabase({ dataSourceId });
onChange(updateOperationLogFilters(value, { field: 'dataSourceId', value: dataSourceId }));
};

const handleDatabaseChange = (databaseName?: string) => {
onChangeSelectDatabase({ databaseName });
onChange(updateOperationLogFilters(value, { field: 'databaseName', value: databaseName }));
};

const handleSchemaChange = (schemaName?: string) => {
onChangeSelectDatabase({ schemaName });
onChange(updateOperationLogFilters(value, { field: 'schemaName', value: schemaName }));
};

return (
<div className={classnames(styles.filters, className)}>
<Select
allowClear
showSearch
className={styles.scopeFilter}
loading={dataSourceList === null}
optionFilterProp="label"
options={dataSourceList || []}
placeholder={i18n('common.dataSource.title')}
size={size}
value={value.dataSourceId}
onChange={handleDataSourceChange}
/>
<Select
allowClear
showSearch
className={styles.scopeFilter}
disabled={!databaseEnabled}
loading={databaseList === null}
optionFilterProp="label"
options={databaseList || []}
placeholder={i18n('common.database.title')}
size={size}
value={value.databaseName}
onChange={handleDatabaseChange}
/>
<Select
allowClear
showSearch
className={styles.scopeFilter}
disabled={!schemaEnabled}
loading={schemaList === null}
optionFilterProp="label"
options={schemaList || []}
placeholder={i18n('common.schema.title')}
size={size}
value={value.schemaName}
onChange={handleSchemaChange}
/>
<Input
allowClear
className={styles.searchFilter}
placeholder={i18n('common.text.searchPlaceholder')}
size={size}
value={value.searchKey || ''}
onChange={(event) =>
onChange(updateOperationLogFilters(value, { field: 'searchKey', value: event.target.value }))
}
/>
</div>
);
}

export default memo(OperationLogFilters);
export type { OperationLogFilterValues } from './model';
export { useDebouncedOperationLogFilters } from './useDebouncedFilters';
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import type { OperationTypeEnum } from '@/service/history';
import {
buildOperationLogListParams,
normalizeOperationLogFilters,
shouldApplyOperationLogPageResponse,
shouldStartOperationLogPageRequest,
updateOperationLogFilters,
} from './model';

const sqlExecute = 'SQL_EXECUTE' as OperationTypeEnum;

{
const filters = normalizeOperationLogFilters({
dataSourceId: 12,
databaseName: ' application ',
schemaName: ' public ',
searchKey: ' Orders ',
});

assert.deepEqual(filters, {
dataSourceId: 12,
databaseName: 'application',
schemaName: 'public',
searchKey: 'Orders',
});
assert.deepEqual(normalizeOperationLogFilters({ databaseName: ' ', searchKey: '\t' }), {});
}

{
const filters = {
dataSourceId: 12,
databaseName: 'application',
schemaName: 'public',
searchKey: 'orders',
};

assert.deepEqual(updateOperationLogFilters(filters, { field: 'databaseName', value: 'analytics' }), {
dataSourceId: 12,
databaseName: 'analytics',
schemaName: undefined,
searchKey: 'orders',
});
assert.deepEqual(updateOperationLogFilters(filters, { field: 'dataSourceId', value: undefined }), {
dataSourceId: undefined,
databaseName: undefined,
schemaName: undefined,
searchKey: 'orders',
});
}

{
assert.deepEqual(
buildOperationLogListParams(
{
dataSourceId: 12,
databaseName: ' application ',
schemaName: ' public ',
searchKey: ' orders ',
},
1,
40,
sqlExecute,
),
{
pageNo: 1,
pageSize: 40,
operationType: sqlExecute,
dataSourceId: 12,
databaseName: 'application',
schemaName: 'public',
searchKey: 'orders',
},
);
}

{
const current = { currentGeneration: 3, finished: false };

// Fresh replace request for the current generation starts.
assert.equal(shouldStartOperationLogPageRequest(3, true, { ...current }), true);
// Append request starts while pages remain.
assert.equal(shouldStartOperationLogPageRequest(3, false, { ...current }), true);
// Stale generation from an older filter change is dropped.
assert.equal(shouldStartOperationLogPageRequest(2, true, { ...current }), false);
// Append is dropped once the stream is finished, but a replace (filter change) still runs.
assert.equal(shouldStartOperationLogPageRequest(3, false, { ...current, finished: true }), false);
assert.equal(shouldStartOperationLogPageRequest(3, true, { ...current, finished: true }), true);
// A request already in flight for the same generation is not duplicated.
assert.equal(shouldStartOperationLogPageRequest(3, true, { ...current, activeRequestGeneration: 3 }), false);
assert.equal(shouldStartOperationLogPageRequest(3, true, { ...current, activeRequestGeneration: 2 }), true);
}

{
const response = { mounted: true, currentGeneration: 5 };

assert.equal(shouldApplyOperationLogPageResponse(5, response), true);
// Response arriving after a filter change (generation bumped) is dropped.
assert.equal(shouldApplyOperationLogPageResponse(4, response), false);
// Response arriving after unmount is dropped.
assert.equal(shouldApplyOperationLogPageResponse(5, { ...response, mounted: false }), false);
}

console.log('Operation log filter tests passed');
109 changes: 109 additions & 0 deletions chat2db-community-client/src/components/OperationLogFilters/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { IGetHistoryListParams, OperationTypeEnum } from '@/service/history';

export interface OperationLogFilterValues {
dataSourceId?: number;
databaseName?: string;
schemaName?: string;
searchKey?: string;
}

export type OperationLogFilterChange =
| { field: 'dataSourceId'; value?: number }
| { field: 'databaseName' | 'schemaName' | 'searchKey'; value?: string };

function normalizeText(value?: string) {
const normalizedValue = value?.trim();
return normalizedValue || undefined;
}

export function normalizeOperationLogFilters(filters: OperationLogFilterValues): OperationLogFilterValues {
const normalizedFilters: OperationLogFilterValues = {};

if (filters.dataSourceId !== undefined) {
normalizedFilters.dataSourceId = filters.dataSourceId;
}

const databaseName = normalizeText(filters.databaseName);
const schemaName = normalizeText(filters.schemaName);
const searchKey = normalizeText(filters.searchKey);

if (databaseName) {
normalizedFilters.databaseName = databaseName;
}
if (schemaName) {
normalizedFilters.schemaName = schemaName;
}
if (searchKey) {
normalizedFilters.searchKey = searchKey;
}

return normalizedFilters;
}

export function updateOperationLogFilters(
filters: OperationLogFilterValues,
change: OperationLogFilterChange,
): OperationLogFilterValues {
if (change.field === 'dataSourceId') {
return {
...filters,
dataSourceId: change.value,
databaseName: undefined,
schemaName: undefined,
};
}

if (change.field === 'databaseName') {
return {
...filters,
databaseName: change.value,
schemaName: undefined,
};
}

return {
...filters,
[change.field]: change.value,
};
}

export interface OperationLogPageRequestState {
currentGeneration: number;
finished: boolean;
activeRequestGeneration?: number;
}

export function shouldStartOperationLogPageRequest(
generation: number,
replace: boolean,
state: OperationLogPageRequestState,
): boolean {
if (generation !== state.currentGeneration) {
return false;
}
if (!replace && state.finished) {
return false;
}
return state.activeRequestGeneration !== generation;
}

export function shouldApplyOperationLogPageResponse(
generation: number,
state: { mounted: boolean; currentGeneration: number },
): boolean {
return state.mounted && generation === state.currentGeneration;
}

export function buildOperationLogListParams(
filters: OperationLogFilterValues,
pageNo: number,
pageSize: number,
operationType: OperationTypeEnum,
): IGetHistoryListParams {
return {
pageNo,
pageSize,
operationType,
...normalizeOperationLogFilters(filters),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css }) => ({
filters: css`
display: flex;
flex: 1;
flex-wrap: wrap;
align-items: center;
gap: 8px;
min-width: 0;
`,
scopeFilter: css`
min-width: 120px;
flex: 1 1 140px;
`,
searchFilter: css`
min-width: 160px;
flex: 2 1 220px;
`,
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useMemo, useState } from 'react';
import { normalizeOperationLogFilters, OperationLogFilterValues } from './model';

export function useDebouncedOperationLogFilters(filters: OperationLogFilterValues, delay = 300) {
const { dataSourceId, databaseName, schemaName, searchKey } = filters;
const [debouncedSearchKey, setDebouncedSearchKey] = useState(normalizeOperationLogFilters({ searchKey }).searchKey);

useEffect(() => {
const timeoutId = window.setTimeout(() => {
setDebouncedSearchKey(normalizeOperationLogFilters({ searchKey }).searchKey);
}, delay);

return () => window.clearTimeout(timeoutId);
}, [delay, searchKey]);

return useMemo(
() =>
normalizeOperationLogFilters({
dataSourceId,
databaseName,
schemaName,
searchKey: debouncedSearchKey,
}),
[dataSourceId, databaseName, debouncedSearchKey, schemaName],
);
}
Loading
Loading