Skip to content

Commit

Permalink
refactor: simplify miller columns (#8025)
Browse files Browse the repository at this point in the history
  • Loading branch information
mintsweet authored Sep 11, 2024
1 parent b2f21ba commit 2a063b2
Show file tree
Hide file tree
Showing 7 changed files with 661 additions and 506 deletions.
9 changes: 8 additions & 1 deletion config-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"dependencies": {
"@ahooksjs/use-url-state": "^3.5.1",
"@ant-design/icons": "^5.3.0",
"@fontsource/roboto": "^5.0.14",
"@mints/miller-columns": "^2.0.0-beta.1",
"@mui/icons-material": "^5.16.7",
"@mui/material": "^5.16.7",
"@mui/styled-engine-sc": "^6.0.0-alpha.18",
"@reduxjs/toolkit": "^2.2.1",
"ahooks": "^3.7.10",
"antd": "^5.14.2",
Expand All @@ -34,7 +39,6 @@
"dayjs": "^1.11.10",
"file-saver": "^2.0.5",
"lodash": "^4.17.21",
"miller-columns-select": "1.4.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
Expand Down Expand Up @@ -69,5 +73,8 @@
"typescript": "^5.1.6",
"vite": "^5.1.4",
"vite-plugin-svgr": "^4.2.0"
},
"resolutions": {
"@mui/styled-engine": "npm:@mui/styled-engine-sc@^6.0.0-alpha.18"
}
}
2 changes: 1 addition & 1 deletion config-ui/src/api/scope/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const searchRemote = (
plugin: string,
connectionId: ID,
data: SearchRemoteQuery,
): Promise<{ children: RemoteScope[]; count: number }> =>
): Promise<{ children: RemoteScope[]; page: number; pageSize: number }> =>
request(`/plugins/${plugin}/connections/${connectionId}/search-remote-scopes`, {
method: 'get',
data,
Expand Down
273 changes: 117 additions & 156 deletions config-ui/src/plugins/components/data-scope-remote/search-local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,50 @@
*
*/

import { useState, useEffect, useMemo } from 'react';
import { useState, useReducer, useCallback } from 'react';
import { CheckCircleFilled, SearchOutlined } from '@ant-design/icons';
import { Space, Tag, Button, Input, Modal, message } from 'antd';
import type { McsID, McsItem, McsColumn } from 'miller-columns-select';
import { MillerColumnsSelect } from 'miller-columns-select';
import { Space, Tag, Button, Input, Modal } from 'antd';
import { MillerColumns } from '@mints/miller-columns';
import { useDebounce } from 'ahooks';

import API from '@/api';
import { Loading, Block, Message } from '@/components';
import { IPluginConfig } from '@/types';
import { Block, Loading, Message } from '@/components';
import type { IPluginConfig } from '@/types';

import * as T from './types';
import * as S from './styled';

type StateType = {
status: string;
scope: any[];
originData: any[];
};

const reducer = (
state: StateType,
action: { type: string; payload?: Pick<Partial<StateType>, 'scope' | 'originData'> },
) => {
switch (action.type) {
case 'LOADING':
return {
...state,
status: 'loading',
};
case 'APPEND':
return {
...state,
scope: [...state.scope, ...(action.payload?.scope ?? [])],
originData: [...state.originData, ...(action.payload?.originData ?? [])],
};
case 'DONE':
return {
...state,
status: 'done',
};
default:
return state;
}
};

interface Props {
mode: 'single' | 'multiple';
plugin: string;
Expand All @@ -40,149 +70,85 @@ interface Props {
onChange: (selectedScope: any[]) => void;
}

let canceling = false;

export const SearchLocal = ({ mode, plugin, connectionId, config, disabledScope, selectedScope, onChange }: Props) => {
const [miller, setMiller] = useState<{
items: McsItem<T.ResItem>[];
loadedIds: ID[];
expandedIds: ID[];
errorId?: ID | null;
nextTokenMap: Record<ID, string>;
}>({
items: [],
loadedIds: [],
expandedIds: [],
nextTokenMap: {},
});

const [open, setOpen] = useState(false);
const [status, setStatus] = useState('init');

const [query, setQuery] = useState('');
const search = useDebounce(query, { wait: 500 });

const scopes = useMemo(
() =>
search
? miller.items
.filter((it) => it.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
.filter((it) => it.type !== 'group')
.map((it) => ({
...it,
parentId: null,
}))
: miller.items,
[search, miller.items],
);
const [search, setSearch] = useState('');

const getItems = async ({
groupId,
currentPageToken,
loadAll,
}: {
groupId: ID | null;
currentPageToken?: string;
loadAll?: boolean;
}) => {
if (canceling) {
canceling = false;
setStatus('init');
return;
}
const [{ status, scope, originData }, dispatch] = useReducer(reducer, {
status: 'idle',
scope: [],
originData: [],
});

let newItems: McsItem<T.ResItem>[] = [];
let nextPageToken = '';
let errorId: ID | null;
const searchDebounce = useDebounce(search, { wait: 500 });

const request = useCallback(
async (groupId?: string | number, params?: any) => {
if (scope.length) {
return {
data: searchDebounce
? scope
.filter((it) => it.title.includes(searchDebounce) && !it.canExpand)
.map((it) => ({ ...it, parentId: null }))
: scope.filter((it) => it.parentId === (groupId ?? null)),
hasMore: status === 'loading' ? true : false,
originData,
};
}

try {
const res = await API.scope.remote(plugin, connectionId, {
groupId,
pageToken: currentPageToken,
groupId: groupId ?? null,
pageToken: params?.nextPageToken,
});

newItems = (res.children ?? []).map((it) => ({
...it,
title: it.name,
const data = res.children.map((it) => ({
parentId: it.parentId,
id: it.id,
title: it.name ?? it.fullName,
canExpand: it.type === 'group',
}));

nextPageToken = res.nextPageToken;
} catch (err: any) {
errorId = groupId;
message.error(err.response.data.message);
}

if (nextPageToken) {
setMiller((m) => ({
...m,
items: [...m.items, ...newItems],
expandedIds: [...m.expandedIds, groupId ?? 'root'],
nextTokenMap: {
...m.nextTokenMap,
[`${groupId ? groupId : 'root'}`]: nextPageToken,
return {
data,
hasMore: !!res.nextPageToken,
params: {
nextPageToken: res.nextPageToken,
},
}));

if (loadAll) {
await getItems({ groupId, currentPageToken: nextPageToken, loadAll });
}
} else {
setMiller((m) => ({
...m,
items: [...m.items, ...newItems],
expandedIds: [...m.expandedIds, groupId ?? 'root'],
loadedIds: [...m.loadedIds, groupId ?? 'root'],
errorId,
}));
originData: res.children,
};
},
[plugin, connectionId, scope, status, searchDebounce],
);

const groupItems = newItems.filter((it) => it.type === 'group');
const handleRequestAll = async () => {
setOpen(false);
dispatch({ type: 'LOADING' });

if (loadAll && groupItems.length) {
groupItems.forEach(async (it) => await getItems({ groupId: it.id, loadAll: true }));
}
}
};
const getData = async (groupId?: string | number, currentPageToken?: string) => {
const res = await API.scope.remote(plugin, connectionId, {
groupId: groupId ?? null,
pageToken: currentPageToken,
});

useEffect(() => {
getItems({ groupId: null });
}, []);
const data = res.children.map((it) => ({
parentId: it.parentId,
id: it.id,
title: it.name ?? it.fullName,
canExpand: it.type === 'group',
}));

useEffect(() => {
if (
miller.items.length &&
!miller.items.filter((it) => it.type === 'group' && !miller.loadedIds.includes(it.id)).length
) {
setStatus('loaded');
}
}, [miller]);
dispatch({ type: 'APPEND', payload: { scope: data, originData: res.children } });

const handleLoadAllScopes = async () => {
setOpen(false);
setStatus('loading');
if (res.nextPageToken) {
await getData(groupId, res.nextPageToken);
}

if (!miller.loadedIds.includes('root')) {
await getItems({
groupId: null,
currentPageToken: miller.nextTokenMap['root'],
loadAll: true,
});
}
await Promise.all(data.filter((it) => it.canExpand).map((it) => getData(it.id)));
};

const noLoadedItems = miller.items.filter((it) => it.type === 'group' && !miller.loadedIds.includes(it.id));
if (noLoadedItems.length) {
noLoadedItems.forEach(async (it) => {
await getItems({
groupId: it.id,
currentPageToken: miller.nextTokenMap[it.id],
loadAll: true,
});
});
}
};
await getData();

const handleCancelLoadAllScopes = () => {
setStatus('cancel');
canceling = true;
dispatch({ type: 'DONE' });
};

return (
Expand All @@ -209,59 +175,54 @@ export const SearchLocal = ({ mode, plugin, connectionId, config, disabledScope,
{(status === 'loading' || status === 'cancel') && (
<S.JobLoad>
<Loading style={{ marginRight: 8 }} size={20} />
Loading: <span className="count">{miller.items.length}</span> scopes found
<Button style={{ marginLeft: 8 }} loading={status === 'cancel'} onClick={handleCancelLoadAllScopes}>
Cancel
</Button>
Loading: <span className="count">{scope.length}</span> scopes found
</S.JobLoad>
)}

{status === 'loaded' && (
{status === 'done' && (
<S.JobLoad>
<CheckCircleFilled style={{ color: '#4DB764' }} />
<span className="count">{miller.items.length}</span> scopes found
<span className="count">{scope.length}</span> scopes found
</S.JobLoad>
)}

{status === 'init' && (
{status === 'idle' && (
<S.JobLoad>
<Button type="primary" disabled={!miller.items.length} onClick={() => setOpen(true)}>
<Button type="primary" onClick={() => setOpen(true)}>
Load all scopes to search by keywords
</Button>
</S.JobLoad>
)}
</Block>
<Block>
{status === 'loaded' && (
<Input prefix={<SearchOutlined />} value={query} onChange={(e) => setQuery(e.target.value)} />
{status === 'done' && (
<Input prefix={<SearchOutlined />} value={search} onChange={(e) => setSearch(e.target.value)} />
)}
<MillerColumnsSelect
mode={mode}
items={scopes}
<MillerColumns
bordered
theme={{
colorPrimary: '#7497f7',
borderColor: '#dbe4fd',
}}
request={request}
columnCount={search ? 1 : config.millerColumn?.columnCount ?? 1}
columnHeight={300}
getCanExpand={(it) => it.type === 'group'}
getHasMore={(id) => !miller.loadedIds.includes(id ?? 'root')}
getHasError={(id) => id === miller.errorId}
onExpand={(id: McsID) => getItems({ groupId: id })}
onScroll={(id: McsID | null) =>
getItems({ groupId: id, currentPageToken: miller.nextTokenMap[id ?? 'root'] })
}
renderTitle={(column: McsColumn) =>
!column.parentId &&
mode={mode}
renderTitle={(id) =>
!id &&
config.millerColumn?.firstColumnTitle && (
<S.ColumnTitle>{config.millerColumn.firstColumnTitle}</S.ColumnTitle>
)
}
renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />}
renderError={() => <span style={{ color: 'red' }}>Something Error</span>}
selectable
disabledIds={(disabledScope ?? []).map((it) => it.id)}
selectedIds={selectedScope.map((it) => it.id)}
onSelectItemIds={(selectedIds: ID[]) => onChange(miller.items.filter((it) => selectedIds.includes(it.id)))}
expandedIds={miller.expandedIds}
onSelectedIds={(ids, data) => onChange((data ?? []).filter((it) => ids.includes(it.id)))}
/>
</Block>
<Modal open={open} centered onOk={handleLoadAllScopes} onCancel={() => setOpen(false)}>
<Modal open={open} centered onOk={handleRequestAll} onCancel={() => setOpen(false)}>
<Message content={`This operation may take a long time, as it iterates through all the ${config.title}.`} />
</Modal>
</>
Expand Down
Loading

0 comments on commit 2a063b2

Please sign in to comment.