This is an automated email from the ASF dual-hosted git repository. mintsweet pushed a commit to branch feat-5640 in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
commit 5ef92f0555ab76542878cf59d606dfd4d77ece23 Author: mintsweet <[email protected]> AuthorDate: Mon Sep 25 15:41:43 2023 +1300 feat(config-ui): use new data-scope-remote to replace old one --- config-ui/src/pages/connection/detail/index.tsx | 4 +- .../api.ts | 0 .../data-scope-remote/data-scope-remote.tsx | 99 ++++++++ .../styled.ts => data-scope-remote/index.ts} | 9 +- .../components/data-scope-remote/search-local.tsx | 248 +++++++++++++++++++++ .../search-remote.tsx} | 96 ++------ .../styled.ts | 10 + .../types.ts | 0 config-ui/src/plugins/components/index.ts | 2 +- config-ui/src/plugins/types.ts | 7 +- 10 files changed, 381 insertions(+), 94 deletions(-) diff --git a/config-ui/src/pages/connection/detail/index.tsx b/config-ui/src/pages/connection/detail/index.tsx index 10b6eaf46..3eb902f93 100644 --- a/config-ui/src/pages/connection/detail/index.tsx +++ b/config-ui/src/pages/connection/detail/index.tsx @@ -26,7 +26,7 @@ import ClearImg from '@/images/icons/clear.svg'; import { ConnectionForm, ConnectionStatus, - DataScopeSelectRemote, + DataScopeRemote, getPluginConfig, getPluginScopeId, ScopeConfigForm, @@ -395,7 +395,7 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) => { } onCancel={handleHideDialog} > - <DataScopeSelectRemote + <DataScopeRemote plugin={plugin} connectionId={connectionId} disabledScope={dataSource} diff --git a/config-ui/src/plugins/components/data-scope-select-remote/api.ts b/config-ui/src/plugins/components/data-scope-remote/api.ts similarity index 100% rename from config-ui/src/plugins/components/data-scope-select-remote/api.ts rename to config-ui/src/plugins/components/data-scope-remote/api.ts diff --git a/config-ui/src/plugins/components/data-scope-remote/data-scope-remote.tsx b/config-ui/src/plugins/components/data-scope-remote/data-scope-remote.tsx new file mode 100644 index 000000000..fc42fc201 --- /dev/null +++ b/config-ui/src/plugins/components/data-scope-remote/data-scope-remote.tsx @@ -0,0 +1,99 @@ +/* + * 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, useMemo } from 'react'; +import { Button, Intent } from '@blueprintjs/core'; + +import { Buttons } from '@/components'; +import { getPluginConfig, getPluginScopeId } from '@/plugins'; +import { operator } from '@/utils'; + +import { SearchLocal } from './search-local'; +import { SearchRemote } from './search-remote'; +import * as API from './api'; + +interface Props { + plugin: string; + connectionId: ID; + disabledScope?: any[]; + onCancel: () => void; + onSubmit: (origin: any) => void; +} + +export const DataScopeRemote = ({ plugin, connectionId, disabledScope, onCancel, onSubmit }: Props) => { + const [selectedScope, setSelectedScope] = useState<any[]>([]); + const [operating, setOperating] = useState(false); + + const config = useMemo(() => getPluginConfig(plugin).dataScope, [plugin]); + + const handleSubmit = async () => { + const [success, res] = await operator( + () => API.updateDataScope(plugin, connectionId, { data: selectedScope.map((it) => it.data) }), + { + setOperating, + formatMessage: () => 'Add data scope successful.', + }, + ); + + if (success) { + onSubmit(res); + } + }; + + return ( + <> + {config.render ? ( + config.render({ + connectionId, + disabledItems: disabledScope?.map((it) => ({ id: getPluginScopeId(plugin, it) })), + selectedItems: selectedScope, + onChangeSelectedItems: setSelectedScope, + }) + ) : config.localSearch ? ( + <SearchLocal + plugin={plugin} + connectionId={connectionId} + config={config} + disabledScope={disabledScope ?? []} + selectedScope={selectedScope} + onChange={setSelectedScope} + /> + ) : ( + <SearchRemote + plugin={plugin} + connectionId={connectionId} + config={config} + disabledScope={disabledScope ?? []} + selectedScope={selectedScope} + onChange={setSelectedScope} + /> + )} + <Buttons position="bottom" align="right"> + <Button outlined intent={Intent.PRIMARY} text="Cancel" disabled={operating} onClick={onCancel} /> + <Button + outlined + intent={Intent.PRIMARY} + text="Save" + loading={operating} + disabled={!selectedScope.length} + onClick={handleSubmit} + /> + </Buttons> + </> + ); +}; diff --git a/config-ui/src/plugins/components/data-scope-select-remote/styled.ts b/config-ui/src/plugins/components/data-scope-remote/index.ts similarity index 83% copy from config-ui/src/plugins/components/data-scope-select-remote/styled.ts copy to config-ui/src/plugins/components/data-scope-remote/index.ts index 41e76ecb3..cf89ce70e 100644 --- a/config-ui/src/plugins/components/data-scope-select-remote/styled.ts +++ b/config-ui/src/plugins/components/data-scope-remote/index.ts @@ -16,11 +16,4 @@ * */ -import styled from 'styled-components'; - -export const Wrapper = styled.div``; - -export const ColumnTitle = styled.div` - padding: 6px 12px; - font-weight: 600; -`; +export * from './data-scope-remote'; diff --git a/config-ui/src/plugins/components/data-scope-remote/search-local.tsx b/config-ui/src/plugins/components/data-scope-remote/search-local.tsx new file mode 100644 index 000000000..3bbb49827 --- /dev/null +++ b/config-ui/src/plugins/components/data-scope-remote/search-local.tsx @@ -0,0 +1,248 @@ +/* + * 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 { Button, InputGroup, Icon, Intent } from '@blueprintjs/core'; +import type { McsID, McsItem, McsColumn } from 'miller-columns-select'; +import { MillerColumnsSelect } from 'miller-columns-select'; +import { useDebounce } from 'ahooks'; + +import { FormItem, MultiSelector, Loading, Dialog, Message } from '@/components'; +import { PluginConfigType } from '@/plugins'; + +import * as T from './types'; +import * as API from './api'; +import * as S from './styled'; + +interface Props { + plugin: string; + connectionId: ID; + config: PluginConfigType['dataScope']; + disabledScope: any[]; + selectedScope: any[]; + onChange: (selectedScope: any[]) => void; +} + +let canceling = false; + +export const SearchLocal = ({ plugin, connectionId, config, disabledScope, selectedScope, onChange }: Props) => { + const [miller, setMiller] = useState<{ + items: McsItem<T.ResItem>[]; + loadedIds: ID[]; + expandedIds: ID[]; + nextTokenMap: Record<ID, string>; + }>({ + items: [], + loadedIds: [], + expandedIds: [], + nextTokenMap: {}, + }); + + const [isOpen, setIsOpen] = 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; + } + + const res = await API.getRemoteScope(plugin, connectionId, { + groupId, + pageToken: currentPageToken, + }); + + const newItems = (res.children ?? []).map((it) => ({ + ...it, + title: it.name, + })); + + if (res.nextPageToken) { + setMiller((m) => ({ + ...m, + items: [...m.items, ...newItems], + expandedIds: [...m.expandedIds, groupId ?? 'root'], + nextTokenMap: { + ...m.nextTokenMap, + [`${groupId ? groupId : 'root'}`]: res.nextPageToken, + }, + })); + + if (loadAll) { + await getItems({ groupId, currentPageToken: res.nextPageToken, loadAll }); + } + } else { + setMiller((m) => ({ + ...m, + items: [...m.items, ...newItems], + expandedIds: [...m.expandedIds, groupId ?? 'root'], + loadedIds: [...m.loadedIds, groupId ?? 'root'], + })); + + 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 () => { + setIsOpen(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 ( + <S.Wrapper> + <FormItem label={config.title} required> + <MultiSelector + disabled + items={selectedScope} + getKey={(it) => it.id} + getName={(it) => it.fullName} + selectedItems={selectedScope} + /> + </FormItem> + <FormItem> + {(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'} + small + text="Cancel" + onClick={handleCancelLoadAllScopes} + /> + </S.JobLoad> + )} + + {status === 'loaded' && ( + <S.JobLoad> + <Icon icon="endorsed" style={{ color: '#4DB764' }} /> + <span className="count">{miller.items.length}</span> scopes found + </S.JobLoad> + )} + + {status === 'init' && ( + <S.JobLoad> + <Button + disabled={!miller.items.length} + intent={Intent.PRIMARY} + text="Load all scopes to search by keywords" + onClick={() => setIsOpen(true)} + /> + </S.JobLoad> + )} + </FormItem> + <FormItem> + {status === 'loaded' && ( + <InputGroup leftIcon="search" value={query} onChange={(e) => setQuery(e.target.value)} /> + )} + <MillerColumnsSelect + items={scopes} + columnCount={search ? 1 : config.millerColumn?.columnCount ?? 1} + columnHeight={300} + getCanExpand={(it) => it.type === 'group'} + getHasMore={(id) => !miller.loadedIds.includes(id ?? 'root')} + 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' }} />} + selectedIds={selectedScope.map((it) => it.id)} + onSelectItemIds={(selectedIds: ID[]) => onChange(miller.items.filter((it) => selectedIds.includes(it.id)))} + expandedIds={miller.expandedIds} + /> + </FormItem> + <Dialog isOpen={isOpen} okText="Load" onCancel={() => setIsOpen(false)} onOk={handleLoadAllScopes}> + <Message content={`This operation may take a long time, as it iterates through all the ${config.title}.`} /> + </Dialog> + </S.Wrapper> + ); +}; diff --git a/config-ui/src/plugins/components/data-scope-select-remote/index.tsx b/config-ui/src/plugins/components/data-scope-remote/search-remote.tsx similarity index 64% rename from config-ui/src/plugins/components/data-scope-select-remote/index.tsx rename to config-ui/src/plugins/components/data-scope-remote/search-remote.tsx index 5be9d3fd4..60f6255e5 100644 --- a/config-ui/src/plugins/components/data-scope-select-remote/index.tsx +++ b/config-ui/src/plugins/components/data-scope-remote/search-remote.tsx @@ -17,15 +17,14 @@ */ import { useEffect, useMemo, useState } from 'react'; -import { Button, Intent, InputGroup } from '@blueprintjs/core'; +import { InputGroup } from '@blueprintjs/core'; import type { McsID, McsItem, McsColumn } from 'miller-columns-select'; import MillerColumnsSelect from 'miller-columns-select'; import { useDebounce } from 'ahooks'; import { uniqBy } from 'lodash'; -import { FormItem, MultiSelector, Loading, Buttons } from '@/components'; -import { getPluginConfig, getPluginScopeId } from '@/plugins'; -import { operator } from '@/utils'; +import { FormItem, MultiSelector, Loading } from '@/components'; +import { PluginConfigType, getPluginScopeId } from '@/plugins'; import * as T from './types'; import * as API from './api'; @@ -34,77 +33,13 @@ import * as S from './styled'; interface Props { plugin: string; connectionId: ID; - disabledScope?: any[]; - onCancel: () => void; - onSubmit: (origin: any) => void; + config: PluginConfigType['dataScope']; + disabledScope: any[]; + selectedScope: any[]; + onChange: (selectedScope: any[]) => void; } -export const DataScopeSelectRemote = ({ plugin, connectionId, disabledScope, onCancel, onSubmit }: Props) => { - const [operating, setOperating] = useState(false); - const [selectedScope, setSelectedScope] = useState<T.ResItem[]>([]); - - const config = useMemo(() => getPluginConfig(plugin).dataScope, [plugin]); - - const handleSubmit = async () => { - const [success, res] = await operator( - () => API.updateDataScope(plugin, connectionId, { data: selectedScope.map((it) => it.data) }), - { - setOperating, - formatMessage: () => 'Add data scope successful.', - }, - ); - - if (success) { - onSubmit(res); - } - }; - - return ( - <> - {config.render ? ( - config.render({ - connectionId, - disabledItems: disabledScope?.map((it) => ({ id: getPluginScopeId(plugin, it) })), - selectedItems: selectedScope, - onChangeSelectedItems: setSelectedScope, - }) - ) : ( - <SelectRemote - plugin={plugin} - connectionId={connectionId} - config={config} - disabledScope={disabledScope} - selectedScope={selectedScope} - onChangeSelectedScope={setSelectedScope} - /> - )} - <Buttons position="bottom" align="right"> - <Button outlined intent={Intent.PRIMARY} text="Cancel" disabled={operating} onClick={onCancel} /> - <Button - outlined - intent={Intent.PRIMARY} - text="Save" - loading={operating} - disabled={!selectedScope.length} - onClick={handleSubmit} - /> - </Buttons> - </> - ); -}; - -const SelectRemote = ({ - plugin, - connectionId, - config, - disabledScope, - selectedScope, - onChangeSelectedScope, -}: Omit<Props, 'onCancel' | 'onSubmit'> & { - config: any; - selectedScope: any[]; - onChangeSelectedScope: (selectedScope: any[]) => void; -}) => { +export const SearchRemote = ({ plugin, connectionId, config, disabledScope, selectedScope, onChange }: Props) => { const [miller, setMiller] = useState<{ items: McsItem<T.ResItem>[]; loadedIds: ID[]; @@ -209,21 +144,22 @@ const SelectRemote = ({ {!searchDebounce ? ( <MillerColumnsSelect items={miller.items} - columnCount={config.millerColumnCount ?? 1} + columnCount={config.millerColumn?.columnCount ?? 1} columnHeight={300} getCanExpand={(it) => it.type === 'group'} getHasMore={(id) => !miller.loadedIds.includes(id ?? 'root')} onExpand={(id: McsID) => getItems(id, miller.nextTokenMap[id])} onScroll={(id: McsID | null) => getItems(id, miller.nextTokenMap[id ?? 'root'])} renderTitle={(column: McsColumn) => - !column.parentId && config.millerFirstTitle && <S.ColumnTitle>{config.millerFirstTitle}</S.ColumnTitle> + !column.parentId && + config.millerColumn?.firstColumnTitle && ( + <S.ColumnTitle>{config.millerColumn.firstColumnTitle}</S.ColumnTitle> + ) } renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />} disabledIds={(disabledScope ?? []).map((it) => getPluginScopeId(plugin, it))} selectedIds={selectedScope.map((it) => it.id)} - onSelectItemIds={(selectedIds: ID[]) => - onChangeSelectedScope(allItems.filter((it) => selectedIds.includes(it.id))) - } + onSelectItemIds={(selectedIds: ID[]) => onChange(allItems.filter((it) => selectedIds.includes(it.id)))} /> ) : ( <MillerColumnsSelect @@ -236,9 +172,7 @@ const SelectRemote = ({ renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />} disabledIds={(disabledScope ?? []).map((it) => getPluginScopeId(plugin, it))} selectedIds={selectedScope.map((it) => it.id)} - onSelectItemIds={(selectedIds: ID[]) => - onChangeSelectedScope(allItems.filter((it) => selectedIds.includes(it.id))) - } + onSelectItemIds={(selectedIds: ID[]) => onChange(allItems.filter((it) => selectedIds.includes(it.id)))} /> )} </FormItem> diff --git a/config-ui/src/plugins/components/data-scope-select-remote/styled.ts b/config-ui/src/plugins/components/data-scope-remote/styled.ts similarity index 87% rename from config-ui/src/plugins/components/data-scope-select-remote/styled.ts rename to config-ui/src/plugins/components/data-scope-remote/styled.ts index 41e76ecb3..27ecd0fe8 100644 --- a/config-ui/src/plugins/components/data-scope-select-remote/styled.ts +++ b/config-ui/src/plugins/components/data-scope-remote/styled.ts @@ -24,3 +24,13 @@ export const ColumnTitle = styled.div` padding: 6px 12px; font-weight: 600; `; + +export const JobLoad = styled.div` + display: flex; + align-items: center; + + & > span.count { + margin: 0 8px; + color: #7497f7; + } +`; diff --git a/config-ui/src/plugins/components/data-scope-select-remote/types.ts b/config-ui/src/plugins/components/data-scope-remote/types.ts similarity index 100% rename from config-ui/src/plugins/components/data-scope-select-remote/types.ts rename to config-ui/src/plugins/components/data-scope-remote/types.ts diff --git a/config-ui/src/plugins/components/index.ts b/config-ui/src/plugins/components/index.ts index 9f3e71026..82d08d3be 100644 --- a/config-ui/src/plugins/components/index.ts +++ b/config-ui/src/plugins/components/index.ts @@ -19,7 +19,7 @@ export * from './connection-form'; export * from './connection-list'; export * from './connection-status'; +export * from './data-scope-remote'; export * from './data-scope-select'; -export * from './data-scope-select-remote'; export * from './scope-config-form'; export * from './scope-config-select'; diff --git a/config-ui/src/plugins/types.ts b/config-ui/src/plugins/types.ts index 589ea9038..b79e6c7c7 100644 --- a/config-ui/src/plugins/types.ts +++ b/config-ui/src/plugins/types.ts @@ -34,9 +34,12 @@ export type PluginConfigType = { fields: any[]; }; dataScope: { + localSearch?: boolean; title?: string; - millerColumnCount?: number; - millerFirstTitle?: string; + millerColumn?: { + columnCount?: number; + firstColumnTitle?: string; + }; render?: (props: any) => React.ReactNode; }; scopeConfig?: {
