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?: {

Reply via email to