blob: 70660c71fc55a412d294e79ce19fa3da6cb5c2ed [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { useState, useEffect, useMemo } 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 { useDebounce } from 'ahooks';
import API from '@/api';
import { Loading, Block, Message } from '@/components';
import { IPluginConfig } from '@/types';
import * as T from './types';
import * as S from './styled';
interface Props {
mode: 'single' | 'multiple';
plugin: string;
connectionId: ID;
config: IPluginConfig['dataScope'];
disabledScope: any[];
selectedScope: any[];
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 getItems = async ({
groupId,
currentPageToken,
loadAll,
}: {
groupId: ID | null;
currentPageToken?: string;
loadAll?: boolean;
}) => {
if (canceling) {
canceling = false;
setStatus('init');
return;
}
let newItems: McsItem<T.ResItem>[] = [];
let nextPageToken = '';
let errorId: ID | null;
try {
const res = await API.scope.remote(plugin, connectionId, {
groupId,
pageToken: currentPageToken,
});
newItems = (res.children ?? []).map((it) => ({
...it,
title: it.name,
}));
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,
},
}));
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,
}));
const groupItems = newItems.filter((it) => it.type === 'group');
if (loadAll && groupItems.length) {
groupItems.forEach(async (it) => await getItems({ groupId: it.id, loadAll: true }));
}
}
};
useEffect(() => {
getItems({ groupId: null });
}, []);
useEffect(() => {
if (
miller.items.length &&
!miller.items.filter((it) => it.type === 'group' && !miller.loadedIds.includes(it.id)).length
) {
setStatus('loaded');
}
}, [miller]);
const handleLoadAllScopes = async () => {
setOpen(false);
setStatus('loading');
if (!miller.loadedIds.includes('root')) {
await getItems({
groupId: null,
currentPageToken: miller.nextTokenMap['root'],
loadAll: true,
});
}
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,
});
});
}
};
const handleCancelLoadAllScopes = () => {
setStatus('cancel');
canceling = true;
};
return (
<>
<Block title={config.title} required>
<Space wrap>
{selectedScope.length ? (
selectedScope.map((sc) => (
<Tag
key={sc.id}
color="blue"
closable
onClose={() => onChange(selectedScope.filter((it) => it.id !== sc.id))}
>
{sc.fullName ?? sc.name}
</Tag>
))
) : (
<span>Please select scope...</span>
)}
</Space>
</Block>
<Block>
{(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>
</S.JobLoad>
)}
{status === 'loaded' && (
<S.JobLoad>
<CheckCircleFilled style={{ color: '#4DB764' }} />
<span className="count">{miller.items.length}</span> scopes found
</S.JobLoad>
)}
{status === 'init' && (
<S.JobLoad>
<Button type="primary" disabled={!miller.items.length} 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)} />
)}
<MillerColumnsSelect
mode={mode}
items={scopes}
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 &&
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>}
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}
/>
</Block>
<Modal open={open} centered onOk={handleLoadAllScopes} onCancel={() => setOpen(false)}>
<Message content={`This operation may take a long time, as it iterates through all the ${config.title}.`} />
</Modal>
</>
);
};