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;
-};

Reply via email to