feat: support on-demand loading of collections from external sources

This commit is contained in:
aaaaaajie 2025-06-20 11:36:30 +08:00
parent a9cde1b168
commit 38cf079b95
5 changed files with 307 additions and 106 deletions

View File

@ -9,7 +9,7 @@
import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react';
import { Button, Table, Checkbox, Input, message, Tooltip } from 'antd'; import { Button, Table, Checkbox, Input, message, Tooltip } from 'antd';
import { ReloadOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { ReloadOutlined, QuestionCircleOutlined, ClearOutlined } from '@ant-design/icons';
import { observer } from '@formily/react'; import { observer } from '@formily/react';
import { useAPIClient } from '@nocobase/client'; import { useAPIClient } from '@nocobase/client';
@ -17,21 +17,64 @@ const CollectionsTable = observer((tableProps: any) => {
const api = useAPIClient(); const api = useAPIClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [pagination, setPagination] = useState({ const [allCollections, setAllCollections] = useState([]);
current: 1, const [selectedMap, setSelectedMap] = useState(new Map());
pageSize: 20, const [selectAllForCurrentView, setSelectAllForCurrentView] = useState(false);
total: 0,
totalPage: 0
});
const searchTimeoutRef = useRef<NodeJS.Timeout>(); const searchTimeoutRef = useRef<NodeJS.Timeout>();
const MAX_SELECTION_LIMIT = 5000;
const { NAMESPACE, t } = tableProps; const { NAMESPACE, t } = tableProps;
const defaultAddAllCollections = const defaultAddAllCollections =
tableProps.formValues?.options?.addAllCollections === undefined tableProps.formValues?.options?.addAllCollections === undefined
? true ? true
: tableProps.formValues?.options?.addAllCollections; : tableProps.formValues?.options?.addAllCollections;
const [addAllCollections, setaddAllCollections] = useState(defaultAddAllCollections); const [addAllCollections, setaddAllCollections] = useState(defaultAddAllCollections);
const collections = tableProps.value || [];
const filteredCollections = useMemo(() => {
if (!searchText.trim()) {
return allCollections;
}
return allCollections.filter((item: any) =>
item.name?.toLowerCase().includes(searchText.toLowerCase())
);
}, [allCollections, searchText]);
const displayCollections = useMemo(() => {
const baseData = tableProps.value && tableProps.value.length > 0 ? tableProps.value : allCollections;
if (!searchText.trim()) {
return baseData;
}
return baseData.filter((item: any) =>
item.name?.toLowerCase().includes(searchText.toLowerCase())
);
}, [tableProps.value, allCollections, searchText]);
const allData = useMemo(() => {
return tableProps.value && tableProps.value.length > 0 ? tableProps.value : allCollections;
}, [tableProps.value, allCollections]);
const enrichedDisplayCollections = useMemo(() => {
return displayCollections.map(item => ({
...item,
selected: selectedMap.get(item.name) || item.required || selectAllForCurrentView
}));
}, [displayCollections, selectedMap, selectAllForCurrentView]);
useEffect(() => {
if (allData.length > 0) {
const newSelectedMap = new Map();
allData.forEach(item => {
if (item.selected) {
newSelectedMap.set(item.name, true);
}
});
setSelectedMap(newSelectedMap);
setSelectAllForCurrentView(false);
}
}, [allData.length]);
const handleAddAllCollectionsChange = useCallback( const handleAddAllCollectionsChange = useCallback(
(checked: boolean) => { (checked: boolean) => {
@ -44,7 +87,7 @@ const CollectionsTable = observer((tableProps: any) => {
); );
const handleLoadCollections = useCallback( const handleLoadCollections = useCallback(
async (page = 1, pageSize = 20, keywords?: string) => { async () => {
const { dataSourceKey: key, formValues, onChange, from } = tableProps; const { dataSourceKey: key, formValues, onChange, from } = tableProps;
const options = formValues?.options || {}; const options = formValues?.options || {};
const requiredText = t('is required'); const requiredText = t('is required');
@ -80,29 +123,17 @@ const CollectionsTable = observer((tableProps: any) => {
const params: any = { const params: any = {
isFirst: from === 'create', isFirst: from === 'create',
dbOptions: { ...options, type: formValues.type || 'mysql' }, dbOptions: { ...options, type: formValues.type || 'mysql' },
page,
pageSize,
}; };
if (keywords) {
params.filter = { keywords };
}
const response = await api.request({ const response = await api.request({
url: `dataSources/${key}/collections:all`, url: `dataSources/${key}/collections:all`,
method: 'get', method: 'get',
params, params,
}); });
const { data, meta } = response?.data || {}; const { data } = response?.data || {};
const collectionsData = data || []; const collectionsData = data || [];
const { count: total, page: current = 0, totalPage } = meta;
setPagination({ setAllCollections(collectionsData);
current,
pageSize,
total,
totalPage
});
if (onChange) { if (onChange) {
onChange(collectionsData); onChange(collectionsData);
@ -121,24 +152,19 @@ const CollectionsTable = observer((tableProps: any) => {
[tableProps.dataSourceKey, tableProps.formValues, tableProps.options, tableProps.from, api, t, NAMESPACE], [tableProps.dataSourceKey, tableProps.formValues, tableProps.options, tableProps.from, api, t, NAMESPACE],
); );
// 防抖搜索
const debouncedSearch = useCallback( const debouncedSearch = useCallback(
(keywords: string) => { (keywords: string) => {
if (addAllCollections) {
return;
}
if (searchTimeoutRef.current) { if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current); clearTimeout(searchTimeoutRef.current);
} }
searchTimeoutRef.current = setTimeout(() => { searchTimeoutRef.current = setTimeout(() => {
handleLoadCollections(1, pagination.pageSize, keywords || undefined); setSearchText(keywords);
}, 500); }, 300);
}, },
[handleLoadCollections, pagination.pageSize], [],
); );
// 清理定时器
useEffect(() => { useEffect(() => {
return () => { return () => {
if (searchTimeoutRef.current) { if (searchTimeoutRef.current) {
@ -147,51 +173,149 @@ const CollectionsTable = observer((tableProps: any) => {
}; };
}, []); }, []);
const { isAllSelected, isIndeterminate } = useMemo(() => { const { isAllSelected, isIndeterminate, selectedCount, isAtLimit } = useMemo(() => {
const selectableCollections = collections.filter((item) => !item.required); const totalSelectedCount = allData.filter(item =>
const selectedCount = collections.filter((item) => item.selected || item.required).length; selectedMap.get(item.name) || item.required
const allSelected = selectableCollections.length > 0 && selectedCount === collections.length; ).length;
const indeterminate = selectedCount > 0 && selectedCount < collections.length;
const atLimit = totalSelectedCount >= MAX_SELECTION_LIMIT;
const selectableCollections = enrichedDisplayCollections.filter((item) => !item.required);
const visibleSelectedCount = enrichedDisplayCollections.filter((item) => item.selected || item.required).length;
const allSelected = selectableCollections.length > 0 && visibleSelectedCount === enrichedDisplayCollections.length;
const indeterminate = visibleSelectedCount > 0 && visibleSelectedCount < enrichedDisplayCollections.length;
return { return {
isAllSelected: allSelected, isAllSelected: allSelected,
isIndeterminate: indeterminate, isIndeterminate: indeterminate,
selectedCount: totalSelectedCount,
isAtLimit: atLimit,
}; };
}, [collections]); }, [enrichedDisplayCollections, allData, selectedMap]);
const handleSelectAll = useCallback( const handleSelectAll = useCallback(
(checked: boolean) => { (checked: boolean) => {
if (checked) {
const currentSelectedCount = allData.filter(item =>
selectedMap.get(item.name) || item.required
).length;
const selectableInCurrentView = displayCollections.filter(item =>
!item.required && !selectedMap.get(item.name)
).length;
if (currentSelectedCount >= MAX_SELECTION_LIMIT) {
return;
}
const remainingLimit = MAX_SELECTION_LIMIT - currentSelectedCount;
if (selectableInCurrentView > remainingLimit) {
message.warning(
t('Maximum selection limit exceeded. To ensure system performance, you can select up to {{limit}} collections.', {
ns: NAMESPACE,
limit: MAX_SELECTION_LIMIT
})
);
}
}
setSelectAllForCurrentView(checked);
const updateCollections = () => { const updateCollections = () => {
const updatedCollections = collections.map((item) => { const newSelectedMap = new Map(selectedMap);
let selectedInThisOperation = 0;
const currentSelectedCount = allData.filter(item =>
selectedMap.get(item.name) || item.required
).length;
const remainingLimit = MAX_SELECTION_LIMIT - currentSelectedCount;
displayCollections.forEach(item => {
if (!item.required) { if (!item.required) {
return { ...item, selected: checked }; if (checked) {
if (selectedInThisOperation < remainingLimit && !selectedMap.get(item.name)) {
newSelectedMap.set(item.name, true);
selectedInThisOperation++;
}
} else {
newSelectedMap.delete(item.name);
}
} }
return item;
}); });
tableProps.onChange?.(updatedCollections);
setSelectedMap(newSelectedMap);
const updatedAllData = allData.map(item => ({
...item,
selected: newSelectedMap.get(item.name) || item.required || false
}));
tableProps.onChange?.(updatedAllData);
setSelectAllForCurrentView(false);
}; };
if (window.requestIdleCallback) { if (typeof MessageChannel !== 'undefined') {
window.requestIdleCallback(updateCollections); const channel = new MessageChannel();
channel.port2.onmessage = () => updateCollections();
channel.port1.postMessage(null);
} else { } else {
setTimeout(updateCollections, 0); setTimeout(updateCollections, 0);
} }
}, },
[collections, tableProps.onChange], [displayCollections, allData, selectedMap, tableProps.onChange, t, NAMESPACE],
); );
const handleSelectChange = useCallback( const handleSelectChange = useCallback(
(index: number, checked: boolean) => { (index: number, checked: boolean) => {
const updatedCollections = [...collections]; const currentItem = enrichedDisplayCollections[index];
updatedCollections[index] = { ...updatedCollections[index], selected: checked }; if (!currentItem || currentItem.selected === checked) return;
tableProps.onChange?.(updatedCollections);
if (checked) {
const currentSelectedCount = allData.filter(item =>
selectedMap.get(item.name) || item.required
).length;
if (currentSelectedCount >= MAX_SELECTION_LIMIT) {
return;
}
if (currentSelectedCount + 1 === MAX_SELECTION_LIMIT) {
message.warning(
t('Maximum selection limit reached. To ensure system performance, you can select up to {{limit}} collections at once.', {
ns: NAMESPACE,
limit: MAX_SELECTION_LIMIT
})
);
}
}
const newSelectedMap = new Map(selectedMap);
if (checked) {
newSelectedMap.set(currentItem.name, true);
} else {
newSelectedMap.delete(currentItem.name);
setSelectAllForCurrentView(false);
}
setSelectedMap(newSelectedMap);
const updateCollections = () => {
const updatedAllData = allData.map(item => ({
...item,
selected: newSelectedMap.get(item.name) || item.required || false
}));
tableProps.onChange?.(updatedAllData);
};
setTimeout(updateCollections, 0);
}, },
[collections, tableProps.onChange], [enrichedDisplayCollections, allData, selectedMap, tableProps.onChange, t, NAMESPACE],
); );
const handleSearch = useCallback( const handleSearch = useCallback(
(value: string) => { (value: string) => {
setSearchText(value);
debouncedSearch(value); debouncedSearch(value);
}, },
[debouncedSearch], [debouncedSearch],
@ -199,57 +323,113 @@ const CollectionsTable = observer((tableProps: any) => {
const handleClearSearch = useCallback(() => { const handleClearSearch = useCallback(() => {
setSearchText(''); setSearchText('');
handleLoadCollections(1, pagination.pageSize); if (searchTimeoutRef.current) {
}, [handleLoadCollections, pagination.pageSize]); clearTimeout(searchTimeoutRef.current);
}
}, []);
const handleTableChange = useCallback( const handleReset = useCallback(() => {
(paginationConfig: any) => { setSelectedMap(new Map());
const { current, pageSize } = paginationConfig; setSelectAllForCurrentView(false);
handleLoadCollections(current, pageSize, searchText || undefined);
}, const updatedAllData = allData.map(item => ({
[handleLoadCollections, searchText], ...item,
); selected: item.required || false
}));
tableProps.onChange?.(updatedAllData);
}, [allData, tableProps.onChange]);
const columns = useMemo(() => { const columns = useMemo(() => {
const CheckboxCell = React.memo(({ selected, required, index, onChange, name, disabled }: any) => (
<Checkbox
checked={required || selected}
disabled={required || disabled}
onChange={(e) => onChange(index, e.target.checked)}
/>
));
const NameCell = React.memo(({ text }: any) => (
<span style={{ paddingLeft: '40px' }}>{text}</span>
));
const baseColumns: any = [ const baseColumns: any = [
{ {
title: <div style={{ textAlign: 'center' }}>{t('Display name', { ns: NAMESPACE })}</div>, title: <div style={{ textAlign: 'center' }}>{t('Display name', { ns: NAMESPACE })}</div>,
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'left' as const, align: 'left' as const,
render: (text: string) => <span style={{ paddingLeft: '40px' }}>{text}</span>, width: '50%',
render: (text: string) => <NameCell text={text} />,
}, },
]; ];
if (!addAllCollections) { if (!addAllCollections) {
const isNearLimit = selectedCount >= MAX_SELECTION_LIMIT * 0.9;
const titleStyle = isNearLimit ? { color: '#ff7a00' } : {};
const currentSelectedCount = allData.filter(item =>
selectedMap.get(item.name) || item.required
).length;
const unselectedInCurrentView = enrichedDisplayCollections.filter(item =>
!item.selected && !item.required
).length;
const canSelectAll = currentSelectedCount + unselectedInCurrentView <= MAX_SELECTION_LIMIT;
baseColumns.push({ baseColumns.push({
title: ( title: (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }}>
<Checkbox <Checkbox
checked={isAllSelected} checked={isAllSelected}
indeterminate={isIndeterminate} indeterminate={isIndeterminate}
disabled={isAtLimit && !isAllSelected && !canSelectAll}
onChange={(e) => handleSelectAll(e.target.checked)} onChange={(e) => handleSelectAll(e.target.checked)}
style={{ marginRight: 4 }}
/> />
{t('Add', { ns: NAMESPACE })} <span style={titleStyle}>
{t('Add', { ns: NAMESPACE })} ({selectedCount}/{allData.length})
</span>
{selectedCount > 0 && (
<Tooltip title={t('Reset selection', { ns: NAMESPACE })}>
<Button
type="text"
size="small"
icon={<ClearOutlined />}
onClick={handleReset}
style={{
padding: '0 4px',
height: '16px',
minWidth: '16px',
fontSize: '12px',
color: '#666'
}}
/>
</Tooltip>
)}
</div> </div>
), ),
dataIndex: 'selected', dataIndex: 'selected',
key: 'selected', key: 'selected',
align: 'center' as const, align: 'center' as const,
width: 150, width: '30%',
render: (selected: boolean, record: any, index: number) => ( render: (selected: boolean, record: any, index: number) => {
<Checkbox const shouldDisable = isAtLimit && !selected && !record.required;
checked={record.required || selected}
disabled={record.required} return (
onChange={(e) => handleSelectChange(index, e.target.checked)} <CheckboxCell
/> selected={selected}
), required={record.required}
index={index}
name={record.name}
disabled={shouldDisable}
onChange={handleSelectChange}
/>
);
},
}); });
} }
return baseColumns; return baseColumns;
}, [t, isAllSelected, isIndeterminate, handleSelectAll, handleSelectChange, addAllCollections, NAMESPACE]); }, [t, isAllSelected, isIndeterminate, handleSelectAll, handleSelectChange, addAllCollections, NAMESPACE, selectedCount, allData.length, isAtLimit, selectedMap, enrichedDisplayCollections, handleReset]);
return ( return (
<div> <div>
@ -265,25 +445,18 @@ const CollectionsTable = observer((tableProps: any) => {
<Checkbox checked={addAllCollections} onChange={(e) => handleAddAllCollectionsChange(e.target.checked)}> <Checkbox checked={addAllCollections} onChange={(e) => handleAddAllCollectionsChange(e.target.checked)}>
{t('Add all collections', { ns: NAMESPACE })} {t('Add all collections', { ns: NAMESPACE })}
</Checkbox> </Checkbox>
<Tooltip
title={t('When there are too many data tables, it may cause system loading lag.', { ns: NAMESPACE })}
placement="right"
>
<QuestionCircleOutlined style={{ color: '#8c8c8c' }} />
</Tooltip>
</div> </div>
<Input.Search <Input.Search
placeholder={t('Search collection name', { ns: NAMESPACE })} placeholder={t('Search collection name', { ns: NAMESPACE })}
allowClear allowClear
style={{ width: 250 }} style={{ width: 250 }}
value={searchText}
onSearch={handleSearch} onSearch={handleSearch}
onChange={(e) => handleSearch(e.target.value)} onChange={(e) => handleSearch(e.target.value)}
onClear={handleClearSearch} onClear={handleClearSearch}
/> />
<Button <Button
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={() => handleLoadCollections(pagination.current, pagination.pageSize, searchText || undefined)} onClick={() => handleLoadCollections()}
loading={loading} loading={loading}
> >
{t('Load Collections', { ns: NAMESPACE })} {t('Load Collections', { ns: NAMESPACE })}
@ -291,24 +464,17 @@ const CollectionsTable = observer((tableProps: any) => {
</div> </div>
<Table <Table
columns={columns} columns={columns}
dataSource={collections} dataSource={enrichedDisplayCollections}
loading={loading} loading={loading}
pagination={{ pagination={false}
current: pagination.current, scroll={{
pageSize: pagination.pageSize, x: addAllCollections ? 300 : 550,
total: pagination.total, y: 400
showSizeChanger: true,
showQuickJumper: false,
showTotal: (total, range) => `${range[0]}-${range[1]} of ${total}`,
pageSizeOptions: ['20', '50', '100'],
size: 'small',
simple: false,
}} }}
scroll={{ x: addAllCollections ? 300 : 550, y: 400 }} virtual
bordered bordered
rowKey="name" rowKey="name"
size="small" size="small"
onChange={handleTableChange}
/> />
</div> </div>
); );

View File

@ -53,12 +53,12 @@ export const EditDatabaseConnectionAction = () => {
field.data = field.data || {}; field.data = field.data || {};
field.data.loading = true; field.data.loading = true;
try { try {
await resource.update({ filterByTk, values: _.omit(form.values, 'collections') });
const toBeAddedCollections = form.values.collections || []; const toBeAddedCollections = form.values.collections || [];
if (!form.values.addAllCollections) { if (!form.values.addAllCollections) {
await addDatasourceCollections(api, filterByTk, { collections: toBeAddedCollections, dbOptions: _.omit(form.values.options, 'collections') }); await addDatasourceCollections(api, filterByTk, { collections: toBeAddedCollections, dbOptions: form.values.options });
} }
delete form.values.collections; delete form.values.collections;
await resource.update({ filterByTk, values: form.values });
ctx.setVisible(false); ctx.setVisible(false);
dm.getDataSource(filterByTk).setOptions(form.values); dm.getDataSource(filterByTk).setOptions(form.values);
dm.getDataSource(filterByTk).reload(); dm.getDataSource(filterByTk).reload();

View File

@ -0,0 +1 @@
export const ALLOW_MAX_COLLECTIONS_COUNT = 5000;

View File

@ -17,13 +17,14 @@ import rolesConnectionResourcesResourcer from './resourcers/data-sources-resourc
import databaseConnectionsRolesResourcer from './resourcers/data-sources-roles'; import databaseConnectionsRolesResourcer from './resourcers/data-sources-roles';
import { rolesRemoteCollectionsResourcer } from './resourcers/roles-data-sources-collections'; import { rolesRemoteCollectionsResourcer } from './resourcers/roles-data-sources-collections';
import { LoadingProgress } from '@nocobase/data-source-manager'; import { DataSourceManager, LoadingProgress } from '@nocobase/data-source-manager';
import lodash from 'lodash'; import lodash from 'lodash';
import { DataSourcesRolesResourcesModel } from './models/connections-roles-resources'; import { DataSourcesRolesResourcesModel } from './models/connections-roles-resources';
import { DataSourcesRolesResourcesActionModel } from './models/connections-roles-resources-action'; import { DataSourcesRolesResourcesActionModel } from './models/connections-roles-resources-action';
import { DataSourceModel } from './models/data-source'; import { DataSourceModel } from './models/data-source';
import { DataSourcesRolesModel } from './models/data-sources-roles-model'; import { DataSourcesRolesModel } from './models/data-sources-roles-model';
import { mergeRole } from '@nocobase/acl'; import { mergeRole } from '@nocobase/acl';
import { ALLOW_MAX_COLLECTIONS_COUNT } from './constants';
type DataSourceState = 'loading' | 'loaded' | 'loading-failed' | 'reloading' | 'reloading-failed'; type DataSourceState = 'loading' | 'loaded' | 'loading-failed' | 'reloading' | 'reloading-failed';
@ -356,6 +357,42 @@ export class PluginDataSourceManagerServer extends Plugin {
await next(); await next();
}); });
this.app.resourceManager.use(async function verifyDatasourceCollectionsCount(ctx, next) {
if (!ctx.action) {
await next();
return;
}
const { actionName, resourceName, params } = ctx.action;
if (resourceName === 'dataSources' && (actionName === 'add' || actionName === 'update')) {
const { values, filterByTk: dataSourceKey } = params;
if (values.options.addAllCollections) {
let introspector: { getCollections: () => Promise<string[]> } = null;
const dataSourceManager = ctx.app['dataSourceManager'] as DataSourceManager;
if (actionName === 'add') {
const klass = dataSourceManager.factory.getClass(values.options.type);
// @ts-ignore
const dataSource = new klass(dbOptions);
introspector = dataSource.collectionManager.dataSource.createDatabaseIntrospector(
dataSource.collectionManager.db,
);
} else {
const dataSource = dataSourceManager.dataSources.get(dataSourceKey);
if (!dataSource) {
throw new Error(`dataSource ${dataSourceKey} not found`);
}
introspector = dataSource['introspector'];
}
const allCollections = await introspector.getCollections();
if (allCollections.length > ALLOW_MAX_COLLECTIONS_COUNT) {
throw new Error(`The number of collections exceeds the limit of ${ALLOW_MAX_COLLECTIONS_COUNT}. Please remove some collections before adding new ones.`);
}
}
}
await next();
});
this.app.use(async function handleAppendDataSourceCollection(ctx, next) { this.app.use(async function handleAppendDataSourceCollection(ctx, next) {
await next(); await next();

View File

@ -11,6 +11,7 @@ import lodash from 'lodash';
import { filterMatch } from '@nocobase/database'; import { filterMatch } from '@nocobase/database';
import _ from 'lodash'; import _ from 'lodash';
import { DataSourceManager } from '@nocobase/data-source-manager'; import { DataSourceManager } from '@nocobase/data-source-manager';
import { ALLOW_MAX_COLLECTIONS_COUNT } from '../constants';
export default { export default {
name: 'dataSources.collections', name: 'dataSources.collections',
@ -129,11 +130,9 @@ export default {
}, },
async all(ctx, next) { async all(ctx, next) {
const params = ctx.action.params; const params = ctx.action.params;
const { associatedIndex: dataSourceKey, isFirst, dbOptions, filter } = params; const { associatedIndex: dataSourceKey, isFirst, dbOptions } = params;
const page = params.page ? Number(params.page) : 1;
const pageSize = params.pageSize ? Number(params.pageSize) : 20;
const dataSourceManager = ctx.app.dataSourceManager as DataSourceManager; const dataSourceManager = ctx.app.dataSourceManager as DataSourceManager;
let introspector: { getCollections: (options: { pageIndex?: number, pageSize?: number, keywords?: string }) => Promise<{ tableList: string[], tableCount: number }> } = null; let introspector: { getCollections: () => Promise<string[]> } = null;
if (isFirst) { if (isFirst) {
const klass = dataSourceManager.factory.getClass(dbOptions.type); const klass = dataSourceManager.factory.getClass(dbOptions.type);
// @ts-ignore // @ts-ignore
@ -148,7 +147,7 @@ export default {
} }
introspector = dataSource['introspector']; introspector = dataSource['introspector'];
} }
const { tableList: allCollections, tableCount } = await introspector.getCollections({ pageIndex: page, pageSize, keywords: filter?.keywords }); const allCollections = await introspector.getCollections();
const selectedCollections = await ctx.db.getRepository('dataSourcesCollections').find({ const selectedCollections = await ctx.db.getRepository('dataSourcesCollections').find({
filter: { dataSourceKey }, filter: { dataSourceKey },
}); });
@ -159,11 +158,7 @@ export default {
selected: !!selectedMap[collection], selected: !!selectedMap[collection],
}; };
}); });
ctx.withoutDataWrapping = true; ctx.body = result;
ctx.body = {
data: result,
meta: { count: tableCount, page, pageSize, totalPage: Math.ceil(tableCount / pageSize) },
}
await next(); await next();
}, },
async add(ctx, next) { async add(ctx, next) {
@ -175,6 +170,9 @@ export default {
await next(); await next();
return; return;
} }
if (collections.length > ALLOW_MAX_COLLECTIONS_COUNT) {
throw new Error(`The number of collections exceeds the limit of ${ALLOW_MAX_COLLECTIONS_COUNT}. Please remove some collections before adding new ones.`);
}
const transaction = await ctx.db.sequelize.transaction(); const transaction = await ctx.db.sequelize.transaction();
const repo = ctx.db.getRepository('dataSourcesCollections'); const repo = ctx.db.getRepository('dataSourcesCollections');
@ -188,7 +186,6 @@ export default {
const incomingCollections = _.keyBy(collections); const incomingCollections = _.keyBy(collections);
const toBeInserted = collections.filter((collection) => !alreadyInsertedNames[collection]); const toBeInserted = collections.filter((collection) => !alreadyInsertedNames[collection]);
const toBeDeleted = Object.keys(alreadyInsertedNames).filter((name) => !incomingCollections[name]); const toBeDeleted = Object.keys(alreadyInsertedNames).filter((name) => !incomingCollections[name]);
if (toBeInserted.length > 0) { if (toBeInserted.length > 0) {
const insertCollections = toBeInserted.map((collection) => { const insertCollections = toBeInserted.map((collection) => {
return { name: collection, dataSourceKey }; return { name: collection, dataSourceKey };