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 8802e1474fa9bc2d5d4b2338767c2cdd7435a900 Author: mintsweet <[email protected]> AuthorDate: Wed Aug 30 16:03:13 2023 +1200 feat(config-ui): support load all data scope in jenkins --- .../components/data-scope-select-remote/api.ts | 6 +- .../components/data-scope-select-remote/index.tsx | 205 +++++++++--------- .../src/plugins/register/jenkins/data-scope.tsx | 232 +++++++++++++++++++-- config-ui/src/plugins/register/jenkins/styled.ts | 12 ++ config-ui/src/plugins/register/jenkins/types.ts | 23 -- 5 files changed, 344 insertions(+), 134 deletions(-) diff --git a/config-ui/src/plugins/components/data-scope-select-remote/api.ts b/config-ui/src/plugins/components/data-scope-select-remote/api.ts index 1d8c79f41..fab503b8e 100644 --- a/config-ui/src/plugins/components/data-scope-select-remote/api.ts +++ b/config-ui/src/plugins/components/data-scope-select-remote/api.ts @@ -20,7 +20,11 @@ import { request } from '@/utils'; import * as T from './types'; -export const getRemoteScope = (plugin: string, connectionId: ID, params: T.GetRemoteScopeParams) => +export const getRemoteScope = ( + plugin: string, + connectionId: ID, + params: T.GetRemoteScopeParams, +): Promise<{ children: T.ResItem[]; nextPageToken: string }> => request(`/plugins/${plugin}/connections/${connectionId}/remote-scopes`, { method: 'get', data: params, diff --git a/config-ui/src/plugins/components/data-scope-select-remote/index.tsx b/config-ui/src/plugins/components/data-scope-select-remote/index.tsx index e454087c4..262756381 100644 --- a/config-ui/src/plugins/components/data-scope-select-remote/index.tsx +++ b/config-ui/src/plugins/components/data-scope-select-remote/index.tsx @@ -41,27 +41,84 @@ interface Props { 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; +}) => { // miller columns const [items, setItems] = useState<McsItem<T.ResItem>[]>([]); - const [selectedItems, setSelectedItems] = useState<T.ResItem[]>([]); const [loadedIds, setLoadedIds] = useState<ID[]>([]); const [nextTokenMap, setNextTokenMap] = useState<Record<ID, string>>({}); // search const [query, setQuery] = useState(''); const [items2, setItems2] = useState<McsItem<T.ResItem>[]>([]); - const [selectedItems2, setSelectedItems2] = useState<T.ResItem[]>([]); const [page, setPage] = useState(1); - const [total, setTotal] = useState(0); + const [total] = useState(0); + const search = useDebounce(query, { wait: 500 }); - const config = useMemo(() => getPluginConfig(plugin).dataScope, [plugin]); - - const selectedScope = useMemo( - () => uniqBy([...selectedItems, ...selectedItems2], 'id'), - [selectedItems, selectedItems2], - ); + const allItems = useMemo(() => uniqBy([...items, ...items2], 'id'), [items, items2]); const getItems = async (groupId: ID | null, currentPageToken?: string) => { const res = await API.getRemoteScope(plugin, connectionId, { @@ -116,93 +173,55 @@ export const DataScopeSelectRemote = ({ plugin, connectionId, disabledScope, onC searchItems(); }, [search, page]); - const handleSubmit = async () => { - const [success, res] = await operator( - () => API.updateDataScope(plugin, connectionId, { data: selectedItems.map((it) => it.data) }), - { - setOperating, - formatMessage: () => 'Add data scope successful.', - }, - ); - - if (success) { - onSubmit(res); - } - }; - return ( <S.Wrapper> - {config.render ? ( - config.render({ - plugin, - connectionId, - disabledItems: (disabledScope ?? []).map((scope) => ({ id: getPluginScopeId(plugin, scope) })), - selectedItems, - onChangeItems: setSelectedItems, - }) - ) : ( - <> - <FormItem label={config.title} required> - <MultiSelector - disabled - items={selectedScope} - getKey={(it) => it.id} - getName={(it) => it.fullName} - selectedItems={selectedScope} - /> - </FormItem> - <FormItem> - <InputGroup leftIcon="search" value={query} onChange={(e) => setQuery(e.target.value)} /> - {!search ? ( - <MillerColumnsSelect - items={items} - columnCount={config.millerColumnCount ?? 1} - columnHeight={300} - getCanExpand={(it) => it.type === 'group'} - getHasMore={(id) => !loadedIds.includes(id ?? 'root')} - onExpand={(id: McsID) => getItems(id, nextTokenMap[id])} - onScroll={(id: McsID | null) => getItems(id, nextTokenMap[id ?? 'root'])} - renderTitle={(column: McsColumn) => - !column.parentId && - config.millerFirstTitle && <S.ColumnTitle>{config.millerFirstTitle}</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[]) => - setSelectedItems(items.filter((it) => selectedIds.includes(it.id))) - } - /> - ) : ( - <MillerColumnsSelect - items={items2} - columnCount={1} - columnHeight={300} - getCanExpand={() => false} - getHasMore={() => total === 0} - onScroll={() => setPage(page + 1)} - renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />} - disabledIds={(disabledScope ?? []).map((it) => getPluginScopeId(plugin, it))} - selectedIds={selectedScope.map((it) => it.id)} - onSelectItemIds={(selectedIds: ID[]) => - setSelectedItems2(items2.filter((it) => selectedIds.includes(it.id))) - } - /> - )} - </FormItem> - </> - )} - <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={!selectedItems.length} - onClick={handleSubmit} + <FormItem label={config.title} required> + <MultiSelector + disabled + items={selectedScope} + getKey={(it) => it.id} + getName={(it) => it.fullName} + selectedItems={selectedScope} /> - </Buttons> + </FormItem> + <FormItem> + <InputGroup leftIcon="search" value={query} onChange={(e) => setQuery(e.target.value)} /> + {!search ? ( + <MillerColumnsSelect + items={items} + columnCount={config.millerColumnCount ?? 1} + columnHeight={300} + getCanExpand={(it) => it.type === 'group'} + getHasMore={(id) => !loadedIds.includes(id ?? 'root')} + onExpand={(id: McsID) => getItems(id, nextTokenMap[id])} + onScroll={(id: McsID | null) => getItems(id, nextTokenMap[id ?? 'root'])} + renderTitle={(column: McsColumn) => + !column.parentId && config.millerFirstTitle && <S.ColumnTitle>{config.millerFirstTitle}</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))) + } + /> + ) : ( + <MillerColumnsSelect + items={items2} + columnCount={1} + columnHeight={300} + getCanExpand={() => false} + getHasMore={() => total === 0} + onScroll={() => setPage(page + 1)} + 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))) + } + /> + )} + </FormItem> </S.Wrapper> ); }; diff --git a/config-ui/src/plugins/register/jenkins/data-scope.tsx b/config-ui/src/plugins/register/jenkins/data-scope.tsx index d7f5be39c..ba147901b 100644 --- a/config-ui/src/plugins/register/jenkins/data-scope.tsx +++ b/config-ui/src/plugins/register/jenkins/data-scope.tsx @@ -16,29 +16,227 @@ * */ -import { DataScopeMillerColumns } from '@/plugins'; +import { useState, useEffect, useMemo } from 'react'; +import { Button, InputGroup, Icon, Intent } from '@blueprintjs/core'; +import type { McsID, McsItem } from 'miller-columns-select'; +import { MillerColumnsSelect } from 'miller-columns-select'; +import { useDebounce } from 'ahooks'; -import type { ScopeItemType } from './types'; +import { FormItem, MultiSelector, Loading, Dialog, Message } from '@/components'; +import * as T from '@/plugins/components/data-scope-select-remote/types'; +import * as API from '@/plugins/components/data-scope-select-remote/api'; + +import * as S from './styled'; interface Props { connectionId: ID; - disabledItems?: ScopeItemType[]; - selectedItems: ScopeItemType[]; - onChangeItems: (selectedItems: ScopeItemType[]) => void; + disabledItems: T.ResItem[]; + selectedItems: T.ResItem[]; + onChangeSelectedItems: (items: T.ResItem[]) => void; } -export const DataScope = ({ connectionId, disabledItems, selectedItems, onChangeItems }: Props) => { +let canceling = false; + +export const DataScope = ({ connectionId, selectedItems, onChangeSelectedItems }: 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 jobs = 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('jenkins', 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 handleLoadAllJobs = 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 handleCancelLoadAllJobs = () => { + setStatus('cancel'); + canceling = true; + }; + return ( - <> - <h3>Jobs *</h3> - <p>Select the jobs you would like to sync.</p> - <DataScopeMillerColumns - plugin="jenkins" - connectionId={connectionId} - disabledItems={disabledItems} - selectedItems={selectedItems} - onChangeItems={onChangeItems} - /> - </> + <S.DataScope> + <FormItem label="Jobs" required> + <MultiSelector + disabled + items={selectedItems} + getKey={(it) => it.id} + getName={(it) => it.fullName} + selectedItems={selectedItems} + /> + </FormItem> + <FormItem> + {(status === 'loading' || status === 'cancel') && ( + <S.JobLoad> + <Loading style={{ marginRight: 8 }} size={20} /> + Loading: <span className="count">{miller.items.length}</span> jobs found + <Button + style={{ marginLeft: 8 }} + loading={status === 'cancel'} + small + text="Cancel" + onClick={handleCancelLoadAllJobs} + /> + </S.JobLoad> + )} + + {status === 'loaded' && ( + <S.JobLoad> + <Icon icon="endorsed" style={{ color: '#4DB764' }} /> + <span className="count">{miller.items.length}</span> jobs found + </S.JobLoad> + )} + + {status === 'init' && ( + <S.JobLoad> + <Button + disabled={!miller.items.length} + intent={Intent.PRIMARY} + text="Load all jobs 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={jobs} + columnCount={search ? 1 : 2.5} + 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'] }) + } + renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />} + selectedIds={selectedItems.map((it) => it.id)} + onSelectItemIds={(selectedIds: ID[]) => + onChangeSelectedItems(miller.items.filter((it) => selectedIds.includes(it.id))) + } + expandedIds={miller.expandedIds} + // onChangeExpandedIds={(expandedIds: ID[]) => setExpandedIds(expandedIds)} + /> + </FormItem> + <Dialog isOpen={isOpen} okText="Load" onCancel={() => setIsOpen(false)} onOk={handleLoadAllJobs}> + <Message content="This operation may take a long time, as it iterates through all the Jenkins Jobs." /> + </Dialog> + </S.DataScope> ); }; diff --git a/config-ui/src/plugins/register/jenkins/styled.ts b/config-ui/src/plugins/register/jenkins/styled.ts index 3de9b32b9..42dc53665 100644 --- a/config-ui/src/plugins/register/jenkins/styled.ts +++ b/config-ui/src/plugins/register/jenkins/styled.ts @@ -48,3 +48,15 @@ export const CICD = styled.div` } } `; + +export const DataScope = styled.div``; + +export const JobLoad = styled.div` + display: flex; + align-items: center; + + & > span.count { + margin: 0 8px; + color: #7497f7; + } +`; diff --git a/config-ui/src/plugins/register/jenkins/types.ts b/config-ui/src/plugins/register/jenkins/types.ts deleted file mode 100644 index 9ac6a76d3..000000000 --- a/config-ui/src/plugins/register/jenkins/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - * - */ - -export type ScopeItemType = { - connectionId: ID; - jobFullName: string; - name: string; -};
