You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by li...@apache.org on 2022/11/15 12:36:23 UTC

[incubator-devlake] branch main updated: feat(config-ui): gitlab project chooses to support the miller-columns component (#3717)

This is an automated email from the ASF dual-hosted git repository.

likyh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new cdc7ef08a feat(config-ui): gitlab project chooses to support the miller-columns component (#3717)
cdc7ef08a is described below

commit cdc7ef08a9064a8353ec932ad4869f31432549f2
Author: 青湛 <0x...@gmail.com>
AuthorDate: Tue Nov 15 20:36:19 2022 +0800

    feat(config-ui): gitlab project chooses to support the miller-columns component (#3717)
    
    * refactor(config-ui): remove content about the miller-columns test
    
    * feat(config-ui): optimize some content about miller-columns
    
    * feat(config-ui): gitlab project chooses to support the miller-columns component
    
    * refactor(config-ui): remove choose all on the miller-columns
    
    * feat(config-ui): optimize component miller-columns
    
    * refactor(config-ui): organize directory about gitlab component
    
    * feat(config-ui): added component project selector about gitlab
    
    * refactor(config-ui): remove content about old gitlab project selector and useGitlab
    
    * feat(config-ui): control gitlab miller columns show on edit blueprint
    
    * refactor(config-ui): remove content related to gitlab api proxy config
    
    * feat(config-ui): optimize checkbox click area
    
    * fix(config-ui): failed to get user's project correctly
    
    * refactor(config-ui): optimize the linkage between miller-columns and project-selector components of gitlab
    
    * feat(config-ui): show parent active on miller-column
    
    * refactor(config-ui): assemble the column component
    
    * feat(config-ui): expand the number of gitlab requests
---
 config-ui/src/App.js                               |  12 --
 .../blueprints/GitlabProjectsSelector.jsx          | 155 -----------------
 .../blueprints/create-workflow/DataScopes.jsx      |  78 +++++++--
 .../checkbox/types.ts => gitlab/config.ts}         |   7 +-
 .../components/{miller-columns => gitlab}/index.ts |   2 +-
 .../src/components/gitlab/miller-columns/index.tsx | 108 ++++++++++++
 .../index.ts => gitlab/miller-columns/styled.ts}   |   7 +-
 .../miller-columns/use-gitlab-miller-columns.ts    |  94 +++++++++++
 .../components/gitlab/project-selector/index.tsx   | 113 +++++++++++++
 .../index.ts => gitlab/project-selector/styled.ts} |   9 +-
 .../use-gitlab-project-selector.ts                 |  98 +++++++++++
 config-ui/src/components/gitlab/request.ts         |  63 +++++++
 .../components/checkbox/checkbox.tsx               |  24 ++-
 .../miller-columns/components/checkbox/styled.ts   |  16 ++
 .../miller-columns/components/checkbox/types.ts    |   3 +-
 .../{item/item-all.tsx => column/column.tsx}       |  28 ++-
 .../components/{ => column}/index.ts               |   3 +-
 .../{ => components/column}/styled.ts              |  34 ++--
 .../components/miller-columns/components/index.ts  |   1 +
 .../miller-columns/components/item/index.ts        |   1 -
 .../miller-columns/components/item/item.tsx        |  23 ++-
 .../miller-columns/components/item/styled.ts       |  34 ++--
 .../src/components/miller-columns/hooks/index.ts   |   2 +-
 .../miller-columns/hooks/use-item-map.ts           |  34 ++--
 .../miller-columns/hooks/use-load-items.ts         | 100 +++++++++++
 .../miller-columns/hooks/use-miller-columns.ts     | 187 ++++++++-------------
 .../components/miller-columns/hooks/use-test.ts    | 116 -------------
 config-ui/src/components/miller-columns/index.ts   |   4 +-
 .../components/miller-columns/miller-columns.tsx   |  74 ++++----
 config-ui/src/components/miller-columns/styled.ts  |  25 +--
 config-ui/src/components/miller-columns/types.ts   |  21 ++-
 config-ui/src/config/gitlabApiProxy.js             |  23 ---
 config-ui/src/hooks/useGitlab.jsx                  | 100 -----------
 config-ui/src/models/GitlabProject.js              |   1 +
 .../src/pages/blueprints/blueprint-settings.jsx    |  25 +--
 .../src/pages/blueprints/create-blueprint.jsx      |  23 ---
 config-ui/tsconfig.json                            |   4 +
 37 files changed, 896 insertions(+), 756 deletions(-)

diff --git a/config-ui/src/App.js b/config-ui/src/App.js
index c4729c450..1fdf94476 100644
--- a/config-ui/src/App.js
+++ b/config-ui/src/App.js
@@ -42,7 +42,6 @@ import BlueprintDetail from '@/pages/blueprints/blueprint-detail'
 import BlueprintSettings from '@/pages/blueprints/blueprint-settings'
 import { IncomingWebhook as IncomingWebhookConnection } from '@/pages/connections/incoming-webhook'
 import MigrationAlertDialog from '@/components/MigrationAlertDialog'
-import { MillerColumns, useTest } from '@/components/miller-columns'
 
 function App(props) {
   const {
@@ -56,8 +55,6 @@ function App(props) {
     handleMigrationDialogClose
   } = useDatabaseMigrations()
 
-  const { items, ids, setIds } = useTest()
-
   return (
     <Router>
       <Route exact path='/'>
@@ -108,15 +105,6 @@ function App(props) {
       <Route exact path='/connections/incoming-webhook'>
         <IncomingWebhookConnection />
       </Route>
-      <Route exact path='/miller-columns'>
-        <MillerColumns
-          height={624}
-          firstColumnTitle='Organizations/Owners'
-          items={items}
-          selectedItemIds={ids}
-          onSelectedItemIds={(ids) => setIds(ids)}
-        />
-      </Route>
       <Route exact path='/offline'>
         <Offline />
       </Route>
diff --git a/config-ui/src/components/blueprints/GitlabProjectsSelector.jsx b/config-ui/src/components/blueprints/GitlabProjectsSelector.jsx
deleted file mode 100644
index 5b17996c5..000000000
--- a/config-ui/src/components/blueprints/GitlabProjectsSelector.jsx
+++ /dev/null
@@ -1,155 +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.
- *
- */
-import React, { useEffect, useState } from 'react'
-import {
-  Checkbox,
-  Intent,
-  MenuItem,
-  Position,
-  Tooltip
-} from '@blueprintjs/core'
-import { MultiSelect } from '@blueprintjs/select'
-import GitlabProject from '@/models/GitlabProject'
-
-const GitlabProjectsSelector = (props) => {
-  const {
-    onFetch = () => [],
-    isFetching = false,
-    configuredConnection,
-    placeholder = 'Select Projects',
-    items = [],
-    selectedItems = [],
-    activeItem = null,
-    disabled = false,
-    isLoading = false,
-    isSaving = false,
-    onItemSelect = () => {},
-    onRemove = () => {},
-    onClear = () => {},
-    itemRenderer = (item, { handleClick, modifiers }) => (
-      <MenuItem
-        active={modifiers.active}
-        disabled={selectedItems.find((i) => i?.id === item?.id)}
-        key={item.value}
-        onClick={handleClick}
-        text={
-          selectedItems.find((i) => i?.id === item?.id) ? (
-            <>
-              <input type='checkbox' checked readOnly /> {item?.title}
-            </>
-          ) : (
-            <span style={{ fontWeight: 700 }}>
-              <input type='checkbox' readOnly /> {item?.title}
-            </span>
-          )
-        }
-        style={{
-          marginBottom: '2px',
-          fontWeight: items.includes(item) ? 700 : 'normal'
-        }}
-      />
-    ),
-    // eslint-disable-next-line max-len
-    tagRenderer = (item) => (
-      <Tooltip
-        intent={Intent.PRIMARY}
-        content={item?.title}
-        position={Position.TOP}
-      >
-        {item.shortTitle || item.title}
-      </Tooltip>
-    )
-  } = props
-
-  const [query, setQuery] = useState('')
-  const [onlyQueryMemberRepo, setOnlyQueryMemberRepo] = useState(true)
-
-  useEffect(() => {
-    // prevent request too frequently
-    const timer = setTimeout(() => {
-      onFetch(query, onlyQueryMemberRepo)
-    }, 200)
-    return () => clearTimeout(timer)
-  }, [onFetch, query, onlyQueryMemberRepo])
-
-  return (
-    <div
-      className='gitlab-projects-multiselect'
-      style={{ display: 'flex', marginBottom: '10px' }}
-    >
-      <div
-        className='gitlab-projects-multiselect-selector'
-        style={{ minWidth: '200px', width: '100%' }}
-      >
-        <MultiSelect
-          disabled={disabled || isSaving || isLoading}
-          // openOnKeyDown={true}
-          resetOnSelect={true}
-          placeholder={placeholder}
-          popoverProps={{ usePortal: false, minimal: true }}
-          className='multiselector-projects'
-          inline={true}
-          fill={true}
-          items={items}
-          selectedItems={selectedItems}
-          activeItem={activeItem}
-          onQueryChange={(query) => setQuery(query)}
-          itemRenderer={itemRenderer}
-          tagRenderer={tagRenderer}
-          tagInputProps={{
-            tagProps: {
-              intent: Intent.PRIMARY,
-              minimal: true
-            }
-          }}
-          noResults={
-            (query.length <= 2 && (
-              <MenuItem
-                disabled={true}
-                text='Please type more than 2 characters to search.'
-              />
-            )) ||
-            (isFetching && <MenuItem disabled={true} text='Fetching...' />) || (
-              <MenuItem disabled={true} text='No Projects Available.' />
-            )
-          }
-          onRemove={(item) => {
-            onRemove(selectedItems.filter((t) => t?.id !== item.id))
-          }}
-          onItemSelect={(item) => {
-            onItemSelect(
-              !selectedItems.includes(item)
-                ? [...selectedItems, new GitlabProject(item)]
-                : selectedItems
-            )
-          }}
-          style={{ borderRight: 0 }}
-        />
-
-        <Checkbox
-          label='Only search my repositories'
-          checked={onlyQueryMemberRepo}
-          onChange={(e) => setOnlyQueryMemberRepo(!onlyQueryMemberRepo)}
-          style={{ margin: '10px 0 0 6px' }}
-        />
-      </div>
-    </div>
-  )
-}
-
-export default GitlabProjectsSelector
diff --git a/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx b/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx
index 3f924e110..9b3a3593c 100644
--- a/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx
+++ b/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx
@@ -30,7 +30,8 @@ import ConnectionTabs from '@/components/blueprints/ConnectionTabs'
 import BoardsSelector from '@/components/blueprints/BoardsSelector'
 import DataDomainsSelector from '@/components/blueprints/DataDomainsSelector'
 import NoData from '@/components/NoData'
-import GitlabProjectsSelector from '@/components/blueprints/GitlabProjectsSelector'
+import { GitLabMillerColumns, GitLabProjectSelector } from '@/components/gitlab'
+import GitlabProject from '@/models/GitlabProject'
 import GitHubProject from '@/models/GithubProject'
 import JenkinsJobsSelector from '@/components/blueprints/JenkinsJobsSelector'
 
@@ -40,9 +41,6 @@ const DataScopes = (props) => {
     activeConnectionTab,
     blueprintConnections = [],
     jiraBoards = [],
-    fetchGitlabProjects = () => [],
-    isFetchingGitlab = false,
-    gitlabProjects = [],
     fetchJenkinsJobs = () => [],
     isFetchingJenkins = false,
     jenkinsJobs = [],
@@ -211,18 +209,66 @@ const DataScopes = (props) => {
                   ) && (
                     <>
                       <h4>Projects *</h4>
-                      <p>Select the project you would like to sync.</p>
-                      <GitlabProjectsSelector
-                        onFetch={fetchGitlabProjects}
-                        isFetching={isFetchingGitlab}
-                        items={gitlabProjects}
-                        selectedItems={selectedScopeEntities}
-                        onItemSelect={setScopeEntities}
-                        onClear={setScopeEntities}
-                        onRemove={setScopeEntities}
-                        disabled={isSaving}
-                        configuredConnection={configuredConnection}
-                        isLoading={isFetching}
+                      {!!activeStep && (
+                        <>
+                          <p>Select the project you would like to sync.</p>
+                          <GitLabMillerColumns
+                            connectionId={configuredConnection.connectionId}
+                            disabledItemIds={selectedScopeEntities
+                              .filter((it) => it.type !== 'miller-columns')
+                              .map((it) => it.id)}
+                            onChangeItems={(items) =>
+                              setScopeEntities([
+                                ...scopeEntitiesGroup[
+                                  configuredConnection.id
+                                ].filter((it) => it.type !== 'miller-columns'),
+                                ...items.map(
+                                  (it) =>
+                                    new GitlabProject({
+                                      ...it,
+                                      type: 'miller-columns'
+                                    })
+                                )
+                              ])
+                            }
+                          />
+                        </>
+                      )}
+                      <div style={{ margin: '16px 0 8px' }}>
+                        Add repositories outside of your projects
+                      </div>
+                      <p>
+                        Enter the repositories using the format “owner/repo” and
+                        separate multiple repos with a comma.
+                      </p>
+                      <GitLabProjectSelector
+                        connectionId={configuredConnection.connectionId}
+                        disabledItemIds={selectedScopeEntities
+                          .filter((it) => it.type !== 'project-selector')
+                          .map((it) => it.id)}
+                        selectedItems={selectedScopeEntities
+                          .filter((it) => it.type === 'project-selector')
+                          .map((it) => ({
+                            id: it.id,
+                            key: it.id,
+                            title: it.title,
+                            shortTitle: it.shortTitle,
+                            value: it.id
+                          }))}
+                        onChangeItems={(items) =>
+                          setScopeEntities([
+                            ...scopeEntitiesGroup[
+                              configuredConnection.id
+                            ].filter((it) => it.type !== 'project-selector'),
+                            ...items.map(
+                              (it) =>
+                                new GitlabProject({
+                                  ...it,
+                                  type: 'project-selector'
+                                })
+                            )
+                          ])
+                        }
                       />
                     </>
                   )}
diff --git a/config-ui/src/components/miller-columns/components/checkbox/types.ts b/config-ui/src/components/gitlab/config.ts
similarity index 86%
copy from config-ui/src/components/miller-columns/components/checkbox/types.ts
copy to config-ui/src/components/gitlab/config.ts
index eb33ae2df..549f4a16d 100644
--- a/config-ui/src/components/miller-columns/components/checkbox/types.ts
+++ b/config-ui/src/components/gitlab/config.ts
@@ -16,8 +16,5 @@
  *
  */
 
-export enum CheckStatus {
-  nochecked = 'nochecked',
-  checked = 'checked',
-  indeterminate = 'indeterminate'
-}
+export const getGitLabProxyApiPrefix = (connectionId: string) =>
+  `/plugins/gitlab/connections/${connectionId}/proxy/rest`
diff --git a/config-ui/src/components/miller-columns/index.ts b/config-ui/src/components/gitlab/index.ts
similarity index 95%
copy from config-ui/src/components/miller-columns/index.ts
copy to config-ui/src/components/gitlab/index.ts
index 07cc2c3f3..0b86f1dd2 100644
--- a/config-ui/src/components/miller-columns/index.ts
+++ b/config-ui/src/components/gitlab/index.ts
@@ -17,4 +17,4 @@
  */
 
 export * from './miller-columns'
-export * from './hooks/use-test'
+export * from './project-selector'
diff --git a/config-ui/src/components/gitlab/miller-columns/index.tsx b/config-ui/src/components/gitlab/miller-columns/index.tsx
new file mode 100644
index 000000000..648d4602a
--- /dev/null
+++ b/config-ui/src/components/gitlab/miller-columns/index.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 React, { useEffect, useState } from 'react'
+
+import type { ColumnType, ItemType } from '@/components/miller-columns'
+import {
+  MillerColumns,
+  ItemStatusEnum,
+  ItemTypeEnum
+} from '@/components/miller-columns'
+
+import {
+  useGitLabMillerColumns,
+  UseGitLabMillerColumnsProps
+} from './use-gitlab-miller-columns'
+import * as S from './styled'
+
+interface Props extends UseGitLabMillerColumnsProps {
+  disabledItemIds?: Array<number>
+  onChangeItems: (
+    items: Array<Pick<ItemType, 'id' | 'title'> & { shortTitle: string }>
+  ) => void
+}
+
+export const GitLabMillerColumns = ({
+  connectionId,
+  disabledItemIds,
+  onChangeItems
+}: Props) => {
+  const [seletedIds, setSelectedIds] = useState<Array<ItemType['id']>>([])
+
+  const { items, itemTree, onExpandItem } = useGitLabMillerColumns<{
+    nameWithNameSpace?: string
+  }>({
+    connectionId
+  })
+
+  useEffect(() => {
+    const curItems = seletedIds
+      .filter((id) => itemTree[id].type === ItemTypeEnum.LEAF)
+      .map((id) => ({
+        id,
+        title: itemTree[id].nameWithNameSpace ?? '',
+        shortTitle: itemTree[id].title
+      }))
+
+    onChangeItems(curItems)
+  }, [seletedIds])
+
+  const renderColumnBottom = ({
+    isLoading,
+    isEmpty
+  }: {
+    isLoading: boolean
+    isEmpty: boolean
+  }) => {
+    switch (true) {
+      case isLoading:
+        return <S.Placeholder>Loading...</S.Placeholder>
+      case isEmpty:
+        return <S.Placeholder>No Data.</S.Placeholder>
+    }
+  }
+
+  return (
+    <MillerColumns
+      height={300}
+      firstColumnTitle='Subgroups/Projects'
+      items={items}
+      disabledItemIds={disabledItemIds}
+      selectedItemIds={seletedIds}
+      onSelectedItemIds={setSelectedIds}
+      onExpandItem={onExpandItem}
+      renderColumnBottom={(col: ColumnType) => {
+        if (!col.parentId) {
+          return renderColumnBottom({
+            isLoading: !itemTree.root,
+            isEmpty: !itemTree.root || !itemTree.root.items.length
+          })
+        } else {
+          return renderColumnBottom({
+            isLoading:
+              !itemTree[col.parentId] ||
+              itemTree[col.parentId].status === ItemStatusEnum.PENDING,
+            isEmpty:
+              !itemTree[col.parentId] || !itemTree[col.parentId].items.length
+          })
+        }
+      }}
+    />
+  )
+}
diff --git a/config-ui/src/components/miller-columns/components/item/index.ts b/config-ui/src/components/gitlab/miller-columns/styled.ts
similarity index 88%
copy from config-ui/src/components/miller-columns/components/item/index.ts
copy to config-ui/src/components/gitlab/miller-columns/styled.ts
index d8175878e..6b7cbc9e7 100644
--- a/config-ui/src/components/miller-columns/components/item/index.ts
+++ b/config-ui/src/components/gitlab/miller-columns/styled.ts
@@ -16,5 +16,8 @@
  *
  */
 
-export * from './item'
-export * from './item-all'
+import styled from '@emotion/styled'
+
+export const Placeholder = styled.div`
+  padding: 4px 12px;
+`
diff --git a/config-ui/src/components/gitlab/miller-columns/use-gitlab-miller-columns.ts b/config-ui/src/components/gitlab/miller-columns/use-gitlab-miller-columns.ts
new file mode 100644
index 000000000..1874f73c1
--- /dev/null
+++ b/config-ui/src/components/gitlab/miller-columns/use-gitlab-miller-columns.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { useMemo, useCallback } from 'react'
+
+import type { ItemType } from '@/components/miller-columns'
+import { useLoadItems, ItemTypeEnum } from '@/components/miller-columns'
+
+import request from '../request'
+import { getGitLabProxyApiPrefix } from '../config'
+
+export interface UseGitLabMillerColumnsProps {
+  connectionId: string
+}
+
+export const useGitLabMillerColumns = <T>({
+  connectionId
+}: UseGitLabMillerColumnsProps) => {
+  const prefix = useMemo(
+    () => getGitLabProxyApiPrefix(connectionId),
+    [connectionId]
+  )
+
+  const upadateGroups = (arr: any): Array<ItemType> =>
+    arr.map((it: any) => ({
+      id: it.id,
+      title: it.name,
+      type: ItemTypeEnum.BRANCH,
+      items: []
+    }))
+
+  const updateProjects = (arr: any): Array<ItemType> =>
+    arr.map((it: any) => ({
+      id: it.id,
+      title: it.name,
+      type: ItemTypeEnum.LEAF,
+      items: [],
+      nameWithNameSpace: it.name_with_namespace
+    }))
+
+  const getInitItems = useCallback(async () => {
+    const user = await request(`${prefix}/user`)
+    const [groups, projects] = await Promise.all([
+      request(`${prefix}/groups`, {
+        data: { top_level_only: 1, per_page: 100 }
+      }),
+      request(`${prefix}/users/${user.id}/projects`, {
+        data: { per_page: 100 }
+      })
+    ])
+    return [...upadateGroups(groups), ...updateProjects(projects)]
+  }, [prefix])
+
+  const loadMoreItems = useCallback(
+    async (item: ItemType) => {
+      const [groups, projects] = await Promise.all([
+        request(`${prefix}/groups/${item.id}/subgroups`, {
+          data: { per_page: 100 }
+        }),
+        request(`${prefix}/groups/${item.id}/projects`, {
+          data: { per_page: 100 }
+        })
+      ])
+      return [...upadateGroups(groups), ...updateProjects(projects)]
+    },
+    [prefix]
+  )
+
+  const { items, itemTree, loadItems } = useLoadItems<T>({
+    getInitItems,
+    loadMoreItems
+  })
+
+  return {
+    items,
+    itemTree,
+    onExpandItem: loadItems
+  }
+}
diff --git a/config-ui/src/components/gitlab/project-selector/index.tsx b/config-ui/src/components/gitlab/project-selector/index.tsx
new file mode 100644
index 000000000..62ca6243c
--- /dev/null
+++ b/config-ui/src/components/gitlab/project-selector/index.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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 React from 'react'
+import { MultiSelect } from '@blueprintjs/select'
+import { Checkbox, MenuItem, Intent } from '@blueprintjs/core'
+
+import type { ItemType } from './use-gitlab-project-selector'
+import {
+  useGitLabProjectSelector,
+  UseGitLabProjectSelectorProps
+} from './use-gitlab-project-selector'
+import * as S from './styled'
+
+interface Props extends UseGitLabProjectSelectorProps {
+  disabledItemIds?: Array<ItemType['id']>
+}
+
+export const GitLabProjectSelector = ({
+  connectionId,
+  disabledItemIds,
+  selectedItems,
+  onChangeItems
+}: Props) => {
+  const {
+    loading,
+    items,
+    search,
+    membership,
+    onSearch,
+    onChangeMembership,
+    onSelect,
+    onRemove
+  } = useGitLabProjectSelector({
+    connectionId,
+    selectedItems,
+    onChangeItems
+  })
+
+  const tagRenderer = (item: any) => {
+    return <span>{item.shortTitle || item.title}</span>
+  }
+
+  const itemRenderer = (item: ItemType, { handleClick }: any) => {
+    const selected = !![
+      ...selectedItems.map((it) => it.id),
+      ...(disabledItemIds ?? [])
+    ].find((id) => id === item.id)
+
+    return (
+      <MenuItem
+        key={item.key}
+        text={
+          <Checkbox label={item.title} checked={selected} disabled={selected} />
+        }
+        disabled={selected}
+        onClick={handleClick}
+      />
+    )
+  }
+
+  return (
+    <S.Container>
+      <MultiSelect
+        className='selector'
+        placeholder='Select Projects'
+        popoverProps={{ usePortal: false, minimal: true, isOpen: !!search }}
+        resetOnSelect
+        fill
+        items={items}
+        selectedItems={selectedItems}
+        tagInputProps={{
+          tagProps: {
+            intent: Intent.PRIMARY,
+            minimal: true
+          }
+        }}
+        noResults={
+          <MenuItem
+            disabled={true}
+            text={loading ? 'Fetching...' : 'No Projects Available.'}
+          />
+        }
+        tagRenderer={tagRenderer}
+        itemRenderer={itemRenderer}
+        onQueryChange={onSearch}
+        onItemSelect={onSelect}
+        onRemove={onRemove}
+      />
+      <Checkbox
+        className='checkbox'
+        label='Only search my repositories'
+        checked={membership}
+        onChange={onChangeMembership}
+      />
+    </S.Container>
+  )
+}
diff --git a/config-ui/src/components/miller-columns/components/item/index.ts b/config-ui/src/components/gitlab/project-selector/styled.ts
similarity index 87%
copy from config-ui/src/components/miller-columns/components/item/index.ts
copy to config-ui/src/components/gitlab/project-selector/styled.ts
index d8175878e..164822357 100644
--- a/config-ui/src/components/miller-columns/components/item/index.ts
+++ b/config-ui/src/components/gitlab/project-selector/styled.ts
@@ -16,5 +16,10 @@
  *
  */
 
-export * from './item'
-export * from './item-all'
+import styled from '@emotion/styled'
+
+export const Container = styled.div`
+  .checkbox {
+    margin: 8px 0 0;
+  }
+`
diff --git a/config-ui/src/components/gitlab/project-selector/use-gitlab-project-selector.ts b/config-ui/src/components/gitlab/project-selector/use-gitlab-project-selector.ts
new file mode 100644
index 000000000..66491d534
--- /dev/null
+++ b/config-ui/src/components/gitlab/project-selector/use-gitlab-project-selector.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 request from '../request'
+import { getGitLabProxyApiPrefix } from '../config'
+
+export type ItemType = {
+  id: number
+  key: number
+  title: string
+  shortTitle: string
+}
+
+export interface UseGitLabProjectSelectorProps {
+  connectionId: string
+  selectedItems: Array<ItemType>
+  onChangeItems: (items: Array<ItemType>) => void
+}
+
+export const useGitLabProjectSelector = ({
+  connectionId,
+  selectedItems,
+  onChangeItems
+}: UseGitLabProjectSelectorProps) => {
+  const [loading, setLoading] = useState(false)
+  const [items, setItems] = useState([])
+  const [search, setSearch] = useState('')
+  const [membership, setMembership] = useState(true)
+
+  const prefix = useMemo(
+    () => getGitLabProxyApiPrefix(connectionId),
+    [connectionId]
+  )
+
+  useEffect(() => {
+    if (!search) return
+    setItems([])
+    setLoading(true)
+
+    const apiPath = `${prefix}/projects`
+
+    const timer = setTimeout(async () => {
+      const res = await request(apiPath, { data: { search, membership } })
+      setItems(
+        res.map((it: any) => ({
+          id: it.id,
+          key: it.id,
+          title: it.name_with_namespace,
+          shortTitle: it.name
+        }))
+      )
+      setLoading(false)
+    }, 1000)
+
+    return () => clearTimeout(timer)
+  }, [prefix, search, membership])
+
+  return useMemo(
+    () => ({
+      loading,
+      items,
+      search,
+      membership,
+      onSearch(s: string) {
+        setSearch(s)
+      },
+      onChangeMembership(e: React.ChangeEvent<HTMLInputElement>) {
+        setMembership(e.target.checked)
+      },
+      onSelect(item: ItemType) {
+        const newItems = [...selectedItems, item]
+        onChangeItems(newItems)
+      },
+      onRemove(item: ItemType) {
+        const newItems = selectedItems.filter((it) => item.id !== it.id)
+        onChangeItems(newItems)
+      }
+    }),
+    [loading, items, search, membership]
+  )
+}
diff --git a/config-ui/src/components/gitlab/request.ts b/config-ui/src/components/gitlab/request.ts
new file mode 100644
index 000000000..c47cb2d7f
--- /dev/null
+++ b/config-ui/src/components/gitlab/request.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 type { AxiosRequestConfig } from 'axios'
+import axios from 'axios'
+
+const instance = axios.create({
+  baseURL: '/api'
+})
+
+export type ReuqestConfig = {
+  method?: AxiosRequestConfig['method']
+  data?: unknown
+  timeout?: number
+  signal?: AbortSignal
+  headers?: Record<string, string>
+}
+
+const request = (path: string, config?: ReuqestConfig) => {
+  const { method = 'GET', data, timeout, headers, signal } = config || {}
+
+  const cancelTokenSource = axios.CancelToken.source()
+  const params: any = {
+    url: path,
+    method,
+    timeout,
+    headers,
+    cancelToken: cancelTokenSource?.token
+  }
+
+  if (method === 'GET') {
+    params.params = data
+  } else {
+    params.data = data
+  }
+
+  const promise = instance.request(params).then((resp) => resp.data)
+
+  if (signal) {
+    signal.addEventListener('abort', () => {
+      cancelTokenSource?.cancel()
+    })
+  }
+
+  return promise
+}
+
+export default request
diff --git a/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx b/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
index a3394e587..a8b7b6649 100644
--- a/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
+++ b/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
@@ -23,20 +23,32 @@ import { CheckStatus } from './types'
 import * as S from './styled'
 
 interface Props {
-  status?: CheckStatus
+  status?: CheckStatus | Array<CheckStatus>
   children?: React.ReactNode
-  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+  onClick?: (e: React.MouseEvent<HTMLLabelElement>) => void
 }
 
 export const Checkbox = ({ children, status, onClick }: Props) => {
   const checkboxCls = classNames('checkbox', {
-    'checkbox-checked': status === CheckStatus.checked,
-    'checkbox-indeterminate': status === CheckStatus.indeterminate
+    'checkbox-checked':
+      status === CheckStatus.checked ||
+      (Array.isArray(status) && status.includes(CheckStatus.checked)),
+    'checkbox-indeterminate':
+      status === CheckStatus.indeterminate ||
+      (Array.isArray(status) && status.includes(CheckStatus.indeterminate)),
+    'checkbox-disabled':
+      status === CheckStatus.disabled ||
+      (Array.isArray(status) && status?.includes(CheckStatus.disabled))
   })
 
+  const handleClick = (e: React.MouseEvent<HTMLLabelElement>) => {
+    if (status === CheckStatus.disabled) return
+    onClick?.(e)
+  }
+
   return (
-    <S.Wrapper>
-      <span className={checkboxCls} onClick={onClick}>
+    <S.Wrapper onClick={handleClick}>
+      <span className={checkboxCls}>
         <span className='checkbox-inner'></span>
       </span>
       {children && <span className='text'>{children}</span>}
diff --git a/config-ui/src/components/miller-columns/components/checkbox/styled.ts b/config-ui/src/components/miller-columns/components/checkbox/styled.ts
index 1eda37188..4d2d2c451 100644
--- a/config-ui/src/components/miller-columns/components/checkbox/styled.ts
+++ b/config-ui/src/components/miller-columns/components/checkbox/styled.ts
@@ -59,6 +59,22 @@ export const Wrapper = styled.label`
       }
     }
 
+    &.checkbox-disabled {
+      cursor: not-allowed;
+
+      .checkbox-inner {
+        background-color: #f5f5f5;
+        border-color: #d9d9d9;
+
+        &::after {
+          background-color: #cccccc;
+          border-color: #f5f5f5;
+          border-collapse: separate;
+          animation-name: none;
+        }
+      }
+    }
+
     .checkbox-input {
       position: absolute;
       z-index: 1;
diff --git a/config-ui/src/components/miller-columns/components/checkbox/types.ts b/config-ui/src/components/miller-columns/components/checkbox/types.ts
index eb33ae2df..b2ab04602 100644
--- a/config-ui/src/components/miller-columns/components/checkbox/types.ts
+++ b/config-ui/src/components/miller-columns/components/checkbox/types.ts
@@ -19,5 +19,6 @@
 export enum CheckStatus {
   nochecked = 'nochecked',
   checked = 'checked',
-  indeterminate = 'indeterminate'
+  indeterminate = 'indeterminate',
+  disabled = 'disabled'
 }
diff --git a/config-ui/src/components/miller-columns/components/item/item-all.tsx b/config-ui/src/components/miller-columns/components/column/column.tsx
similarity index 62%
rename from config-ui/src/components/miller-columns/components/item/item-all.tsx
rename to config-ui/src/components/miller-columns/components/column/column.tsx
index a0dc0cb95..967cf005d 100644
--- a/config-ui/src/components/miller-columns/components/item/item-all.tsx
+++ b/config-ui/src/components/miller-columns/components/column/column.tsx
@@ -18,28 +18,24 @@
 
 import React from 'react'
 
-import type { ColumnType } from '../../types'
-
-import { Checkbox, CheckStatus } from '../checkbox'
+import type { ItemType } from '../../types'
 
 import * as S from './styled'
 
 interface Props {
-  column: ColumnType
-  checkStatus?: CheckStatus
-  onSelectAllItem?: (column: ColumnType) => void
+  items: Array<ItemType>
+  renderItem: (item: ItemType) => React.ReactNode
+  height?: number
+  title?: string | React.ReactNode
+  bottom?: React.ReactNode
 }
 
-export const ItemAll = ({ column, checkStatus, onSelectAllItem }: Props) => {
-  const handleCheckboxClick = (e: React.MouseEvent<HTMLDivElement>) => {
-    e.stopPropagation()
-    onSelectAllItem?.(column)
-  }
-
+export const Column = ({ items, renderItem, height, title, bottom }: Props) => {
   return (
-    <S.Wrapper selected={false}>
-      <Checkbox status={checkStatus} onClick={handleCheckboxClick} />
-      <span>All</span>
-    </S.Wrapper>
+    <S.Container height={height}>
+      {title && <div className='title'>{title}</div>}
+      {items.map((it) => renderItem(it))}
+      {bottom}
+    </S.Container>
   )
 }
diff --git a/config-ui/src/components/miller-columns/components/index.ts b/config-ui/src/components/miller-columns/components/column/index.ts
similarity index 94%
copy from config-ui/src/components/miller-columns/components/index.ts
copy to config-ui/src/components/miller-columns/components/column/index.ts
index 25c3b9507..c9f2a97c2 100644
--- a/config-ui/src/components/miller-columns/components/index.ts
+++ b/config-ui/src/components/miller-columns/components/column/index.ts
@@ -16,5 +16,4 @@
  *
  */
 
-export * from './checkbox'
-export * from './item'
+export * from './column'
diff --git a/config-ui/src/components/miller-columns/styled.ts b/config-ui/src/components/miller-columns/components/column/styled.ts
similarity index 72%
copy from config-ui/src/components/miller-columns/styled.ts
copy to config-ui/src/components/miller-columns/components/column/styled.ts
index f22fb81d2..b91d76491 100644
--- a/config-ui/src/components/miller-columns/styled.ts
+++ b/config-ui/src/components/miller-columns/components/column/styled.ts
@@ -19,28 +19,22 @@
 import styled from '@emotion/styled'
 
 export const Container = styled.div<{ height?: number }>`
-  display: flex;
-  width: 100%;
+  flex: 0 0 33.33%;
+  margin: 0;
+  padding: 0;
+  width: 33.33%;
   ${({ height }) => `height: ${height}px;`}
-  overflow-x: auto;
+  list-style: none;
+  border-left: 1px solid #dbe4fd;
+  overflow-y: auto;
 
-  .items {
-    flex: 1;
-    margin: 0;
-    padding: 0;
-    list-style: none;
-    border: 1px solid #dbe4fd;
-    border-right: none;
-    border-radius: 4px;
-
-    &:last-child {
-      border-right: 1px solid #dbe4fd;
-    }
+  &:first-child {
+    border-left: none;
+  }
 
-    & > .title {
-      padding: 4px 12px;
-      font-weight: 700;
-      color: #292b3f;
-    }
+  & > .title {
+    padding: 4px 12px;
+    font-weight: 700;
+    color: #292b3f;
   }
 `
diff --git a/config-ui/src/components/miller-columns/components/index.ts b/config-ui/src/components/miller-columns/components/index.ts
index 25c3b9507..806888c42 100644
--- a/config-ui/src/components/miller-columns/components/index.ts
+++ b/config-ui/src/components/miller-columns/components/index.ts
@@ -18,3 +18,4 @@
 
 export * from './checkbox'
 export * from './item'
+export * from './column'
diff --git a/config-ui/src/components/miller-columns/components/item/index.ts b/config-ui/src/components/miller-columns/components/item/index.ts
index d8175878e..0c230080b 100644
--- a/config-ui/src/components/miller-columns/components/item/index.ts
+++ b/config-ui/src/components/miller-columns/components/item/index.ts
@@ -17,4 +17,3 @@
  */
 
 export * from './item'
-export * from './item-all'
diff --git a/config-ui/src/components/miller-columns/components/item/item.tsx b/config-ui/src/components/miller-columns/components/item/item.tsx
index ca71d17cc..172e39534 100644
--- a/config-ui/src/components/miller-columns/components/item/item.tsx
+++ b/config-ui/src/components/miller-columns/components/item/item.tsx
@@ -18,7 +18,7 @@
 
 import React from 'react'
 
-import { ItemType, RowStatus } from '../../types'
+import { ItemType, ItemTypeEnum, RowStatus } from '../../types'
 
 import { Checkbox, CheckStatus } from '../checkbox'
 
@@ -27,7 +27,7 @@ import * as S from './styled'
 interface Props {
   item: ItemType
   status?: RowStatus
-  checkStatus?: CheckStatus
+  checkStatus?: CheckStatus | Array<CheckStatus>
   checkedCount?: number
   onExpandItem?: (it: ItemType) => void
   onSelectItem?: (it: ItemType) => void
@@ -37,7 +37,6 @@ export const Item = ({
   item,
   status = RowStatus.noselected,
   checkStatus = CheckStatus.nochecked,
-  checkedCount = 0,
   onExpandItem,
   onSelectItem
 }: Props) => {
@@ -45,23 +44,23 @@ export const Item = ({
     onExpandItem?.(item)
   }
 
-  const handleCheckboxClick = (e: React.MouseEvent<HTMLDivElement>) => {
-    e.stopPropagation()
+  const handleCheckboxClick = (e: React.MouseEvent<HTMLLabelElement>) => {
+    if (item.type === ItemTypeEnum.LEAF) {
+      e.stopPropagation()
+    }
     onSelectItem?.(item)
   }
 
   return (
     <S.Wrapper
+      type={item.type}
       selected={status === RowStatus.selected}
       onClick={handleRowClick}
     >
-      <Checkbox status={checkStatus} onClick={handleCheckboxClick} />
-      <span className='title'>{item.title}</span>
-      {!!item.total && (
-        <span className='count'>
-          ({checkedCount}/{item.total})
-        </span>
-      )}
+      <Checkbox status={checkStatus} onClick={handleCheckboxClick}>
+        {item.title}
+      </Checkbox>
+      {item.type === ItemTypeEnum.BRANCH && <span className='indicator' />}
     </S.Wrapper>
   )
 }
diff --git a/config-ui/src/components/miller-columns/components/item/styled.ts b/config-ui/src/components/miller-columns/components/item/styled.ts
index 7c2dab4b8..ca7b3476c 100644
--- a/config-ui/src/components/miller-columns/components/item/styled.ts
+++ b/config-ui/src/components/miller-columns/components/item/styled.ts
@@ -18,23 +18,33 @@
 
 import styled from '@emotion/styled'
 
-export const Wrapper = styled.div<{ selected: boolean }>`
+import { ItemTypeEnum } from '../../types'
+
+export const Wrapper = styled.div<{ selected: boolean; type: ItemTypeEnum }>`
   display: flex;
   align-items: center;
+  justify-content: space-between;
   padding: 4px 12px;
-  cursor: pointer;
-
-  ${({ selected }) => (selected ? 'background-color: #f5f5f7;' : '')}
 
-  &:hover {
-    background-color: #f5f5f7;
-  }
+  ${({ type }) =>
+    type === ItemTypeEnum.BRANCH
+      ? `
+    cursor: pointer;
+    &:hover {
+      background-color: #f5f5f7;
+    }
+    `
+      : ''}
 
-  & > span.name {
-    font-size: 14px;
-  }
+  ${({ selected }) => (selected ? 'background-color: #f5f5f7;' : '')}
 
-  & > span.count {
-    font-size: 14px;
+  & > span.indicator {
+    display: table;
+    width: 6px;
+    height: 6px;
+    border: 1px solid #000;
+    border-top: 0;
+    border-left: 0;
+    transform: rotate(-45deg);
   }
 `
diff --git a/config-ui/src/components/miller-columns/hooks/index.ts b/config-ui/src/components/miller-columns/hooks/index.ts
index d9c9dbfb1..25195b787 100644
--- a/config-ui/src/components/miller-columns/hooks/index.ts
+++ b/config-ui/src/components/miller-columns/hooks/index.ts
@@ -18,5 +18,5 @@
 
 export * from './use-columns'
 export * from './use-item-map'
+export * from './use-load-items'
 export * from './use-miller-columns'
-export * from './use-test'
diff --git a/config-ui/src/components/miller-columns/hooks/use-item-map.ts b/config-ui/src/components/miller-columns/hooks/use-item-map.ts
index ad62205c0..89113d148 100644
--- a/config-ui/src/components/miller-columns/hooks/use-item-map.ts
+++ b/config-ui/src/components/miller-columns/hooks/use-item-map.ts
@@ -19,13 +19,23 @@
 import { useMemo } from 'react'
 
 import type { ItemType, ItemInfoType } from '../types'
+import { ItemStatusEnum } from '../types'
 
 interface Props {
   items: ItemType[]
-  selectedItemIds?: Array<ItemType['id']>
 }
 
-export const useItemMap = ({ items, selectedItemIds = [] }: Props) => {
+export const useItemMap = ({ items }: Props) => {
+  const checkChildLoaded = (item: ItemType): boolean => {
+    if (item.status === ItemStatusEnum.PENDING) {
+      return false
+    }
+
+    return item.items.every((it) => {
+      return checkChildLoaded(it)
+    })
+  }
+
   return useMemo(() => {
     const itemMap = new Map<ItemType['id'], ItemInfoType>()
 
@@ -40,7 +50,7 @@ export const useItemMap = ({ items, selectedItemIds = [] }: Props) => {
         itemMap.set(item.id, {
           item,
           parentId: parent?.id,
-          selectedChildCount: 0
+          childLoaded: checkChildLoaded(item)
         })
       }
 
@@ -50,30 +60,18 @@ export const useItemMap = ({ items, selectedItemIds = [] }: Props) => {
     }
 
     items.forEach((it) => collect({ item: it }))
-    selectedItemIds.forEach((id) => {
-      const childTotal = itemMap.get(id)?.item.total ?? 0
-      const addedCount = childTotal + 1
-      const parentId = itemMap.get(id)?.parentId
-      const parent = parentId ? itemMap.get(parentId) : null
-      if (parent) {
-        parent.selectedChildCount += addedCount
-      }
-    })
 
     return {
       getItem(id: ItemType['id']) {
         return (itemMap.get(id) as ItemInfoType).item
       },
-      getItemSelectedChildCount(id: ItemType['id']) {
-        return (itemMap.get(id) as ItemInfoType).selectedChildCount
-      },
       getItemParent(id: ItemType['id']) {
         const parentId = itemMap.get(id)?.parentId
         return parentId ? (itemMap.get(parentId) as ItemInfoType).item : null
       },
-      getItemMapSize() {
-        return itemMap.size
+      getItemChildLoaded(id: ItemType['id']) {
+        return (itemMap.get(id) as ItemInfoType).childLoaded
       }
     }
-  }, [items, selectedItemIds])
+  }, [items])
 }
diff --git a/config-ui/src/components/miller-columns/hooks/use-load-items.ts b/config-ui/src/components/miller-columns/hooks/use-load-items.ts
new file mode 100644
index 000000000..1a75873b8
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-load-items.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { ItemType, ItemTypeEnum, ItemStatusEnum } from '../types'
+
+interface Props {
+  getInitItems: () => Promise<Array<ItemType>>
+  loadMoreItems: (item: ItemType) => Promise<Array<ItemType>>
+}
+
+type TreeType<T> = Record<ItemType['id'], ItemType & T>
+
+export const useLoadItems = <T>({ getInitItems, loadMoreItems }: Props) => {
+  const [tree, setTree] = useState<TreeType<T>>({})
+
+  const itemsToTree = (items: Array<ItemType>) => {
+    return items.reduce((acc, cur) => {
+      acc[cur.id] = {
+        ...cur,
+        items: [],
+        status:
+          cur.type === ItemTypeEnum.BRANCH
+            ? ItemStatusEnum.PENDING
+            : ItemStatusEnum.READY
+      }
+      return acc
+    }, {} as any)
+  }
+
+  const treeToItems = (t: TreeType<T>) => {
+    if (!t.root) {
+      return []
+    }
+
+    const transform = (arr: Array<ItemType>): Array<ItemType> => {
+      return arr.map((it) => ({
+        ...it,
+        ...t[it.id],
+        items: transform(t[it.id].items)
+      }))
+    }
+
+    return transform(t.root.items)
+  }
+
+  useEffect(() => {
+    ;(async () => {
+      const initItems = await getInitItems()
+      setTree({
+        root: {
+          id: 'root',
+          title: 'root',
+          type: ItemTypeEnum.BRANCH,
+          status: ItemStatusEnum.READY,
+          items: initItems
+        },
+        ...itemsToTree(initItems)
+      })
+    })()
+  }, [])
+
+  return useMemo(() => {
+    return {
+      items: treeToItems(tree),
+      itemTree: tree,
+      async loadItems(item: ItemType) {
+        if (tree[item.id].status === ItemStatusEnum.READY) {
+          return
+        }
+        const items = await loadMoreItems(item)
+        setTree({
+          ...tree,
+          [`${item.id}`]: {
+            ...item,
+            items,
+            status: ItemStatusEnum.READY
+          },
+          ...itemsToTree(items)
+        })
+      }
+    }
+  }, [tree])
+}
diff --git a/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts b/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts
index 837f437c4..bc57cf79c 100644
--- a/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts
+++ b/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts
@@ -19,7 +19,7 @@
 import { useState, useMemo, useEffect } from 'react'
 
 import type { ItemType, ColumnType } from '../types'
-import { RowStatus } from '../types'
+import { ItemTypeEnum, RowStatus } from '../types'
 import { CheckStatus } from '../components'
 
 import { useItemMap } from './use-item-map'
@@ -29,21 +29,26 @@ export interface UseMillerColumnsProps {
   items: ItemType[]
   activeItemId?: ItemType['id']
   onActiveItemId?: (id: ItemType['id']) => void
-  selectedItemIds?: Array<ItemType['id']>
+  disabledItemIds?: Array<ItemType['id']>
+  selectedItemIds: Array<ItemType['id']>
   onSelectedItemIds?: (ids: Array<ItemType['id']>) => void
+  onExpandItem?: (item: ItemType) => void
 }
 
 export const useMillerColumns = ({
   items,
+  disabledItemIds,
   onActiveItemId,
   onSelectedItemIds,
+  onExpandItem,
   ...props
 }: UseMillerColumnsProps) => {
   const [activeItemId, setActiveItemId] = useState<ItemType['id']>()
-  const [selectedItemIds, setSelectedItemIds] =
-    useState<Array<ItemType['id']>>()
+  const [selectedItemIds, setSelectedItemIds] = useState<Array<ItemType['id']>>(
+    []
+  )
 
-  const itemMap = useItemMap({ items, selectedItemIds })
+  const itemMap = useItemMap({ items })
   const columns = useColumns({ items, itemMap, activeItemId })
 
   useEffect(() => {
@@ -51,148 +56,100 @@ export const useMillerColumns = ({
   }, [props.activeItemId])
 
   useEffect(() => {
-    setSelectedItemIds(props.selectedItemIds)
+    setSelectedItemIds(props.selectedItemIds ?? [])
   }, [props.selectedItemIds])
 
+  const collectAddParentIds = (item: ItemType) => {
+    let result: Array<ItemType['id']> = []
+
+    const parentItem = itemMap.getItemParent(item.id)
+
+    if (parentItem) {
+      const childSelectedIds = parentItem.items
+        .map((it) => it.id)
+        .filter((id) => [...selectedItemIds, item.id].includes(id))
+
+      if (childSelectedIds.length === parentItem.items.length) {
+        result.push(parentItem.id)
+        result.push(...collectAddParentIds(parentItem))
+      }
+    }
+
+    return result
+  }
+
+  const collectRemoveParentIds = (item: ItemType) => {
+    let result: Array<ItemType['id']> = []
+
+    const parentItem = itemMap.getItemParent(item.id)
+
+    if (parentItem) {
+      result.push(parentItem.id)
+      result.push(...collectRemoveParentIds(parentItem))
+    }
+
+    return result
+  }
+
   return useMemo(
     () => ({
       columns,
       itemMap,
       activeItemId,
       selectedItemIds,
-      getStatus(item: ItemType) {
-        if (item.id === activeItemId) {
+      getStatus(item: ItemType, column: ColumnType) {
+        if (column.activeId === item.id) {
           return RowStatus.selected
         }
         return RowStatus.noselected
       },
       getChekecdStatus(item: ItemType) {
-        if (!selectedItemIds?.length) {
-          return CheckStatus.nochecked
-        }
-        if (selectedItemIds?.includes(item.id)) {
-          return CheckStatus.checked
-        }
+        const childSelectedIds = item.items
+          .map((it) => it.id)
+          .filter((id) => selectedItemIds.includes(id))
 
-        const hasChildCheckedIds = selectedItemIds.filter((id) =>
-          item.items?.map((it) => it.id).includes(id)
-        )
-
-        if (!hasChildCheckedIds.length) {
-          return CheckStatus.nochecked
-        }
-
-        if (hasChildCheckedIds.length === item.items?.length) {
-          return CheckStatus.checked
-        }
-
-        return CheckStatus.indeterminate
-      },
-      getCheckedAllStatus(column: ColumnType) {
-        const itemIds = column.items?.map((it) => it.id) ?? []
-        const colSelectedIds = itemIds.filter((id) =>
-          selectedItemIds?.includes(id)
-        )
         switch (true) {
-          case colSelectedIds.length === itemIds.length:
+          case !itemMap.getItemChildLoaded(item.id):
+          case (disabledItemIds ?? []).includes(item.id):
+            return CheckStatus.disabled
+          case selectedItemIds.includes(item.id):
             return CheckStatus.checked
-          case !!colSelectedIds.length:
+          case !!childSelectedIds.length:
             return CheckStatus.indeterminate
           default:
             return CheckStatus.nochecked
         }
       },
-      getCheckedCount(item: ItemType) {
-        return itemMap.getItemSelectedChildCount(item.id)
-      },
       onExpandItem(item: ItemType) {
-        if (!item.items?.length) {
+        if (item.type !== ItemTypeEnum.BRANCH) {
           return
         }
+        onExpandItem?.(item)
         onActiveItemId ? onActiveItemId(item.id) : setActiveItemId(item.id)
       },
       onSelectItem(item: ItemType) {
-        let newIds: Array<ItemType['id']>
-        let targetIds: Array<ItemType['id']> = [item.id]
-        const itemIds = item.items?.map((it) => it.id) ?? []
-
-        const collect = (id: ItemType['id']) => {
-          targetIds.push(id)
-          const item = itemMap.getItem(id)
-          if (item.items) {
-            item.items.forEach((it) => collect(it.id))
-          }
-        }
-
-        itemIds.forEach((id) => collect(id))
-
-        const isRemoveExistedItem = !!selectedItemIds?.includes(item.id)
-
-        if (isRemoveExistedItem) {
-          const parentItem = itemMap.getItemParent(item.id)
-          const deleteIds = [parentItem?.id, ...targetIds].filter(Boolean)
-          newIds =
-            selectedItemIds?.filter((id) => !deleteIds.includes(id)) ?? []
-        } else {
-          const parentItem = itemMap.getItemParent(item.id)
-          const addIds = targetIds.filter(
-            (id) => !selectedItemIds?.includes(id)
-          )
-
-          if (parentItem) {
-            const parentChildIds = parentItem.items?.map((it) => it.id) ?? []
-            const parentSelectedIds = parentChildIds.filter((id) =>
-              [...(selectedItemIds ?? []), item.id].includes(id)
-            )
-
-            const isAllChildSelected =
-              parentSelectedIds.length === parentItem?.items?.length
-
-            if (isAllChildSelected) {
-              addIds.push(parentItem.id)
-            }
-          }
+        let newIds: Array<ItemType['id']> = [item.id]
+        const isRemoveExistedItem = !!selectedItemIds.includes(item.id)
 
-          newIds = [...(selectedItemIds ?? []), ...addIds]
+        const collectChildIds = (it: ItemType) => {
+          newIds.push(it.id)
+          it.items.forEach((it) => collectChildIds(it))
         }
 
-        onSelectedItemIds
-          ? onSelectedItemIds(newIds)
-          : setSelectedItemIds(newIds)
-      },
-      onSelectAllItem(column: ColumnType) {
-        let newIds: Array<ItemType['id']>
-        let targetIds: Array<ItemType['id']> = []
-        const itemIds = column.items?.map((it) => it.id) ?? []
-
-        const collect = (id: ItemType['id']) => {
-          targetIds.push(id)
-          const item = itemMap.getItem(id)
-          if (item.items) {
-            item.items.forEach((it) => collect(it.id))
-          }
-        }
+        item.items.forEach((it) => collectChildIds(it))
 
-        itemIds.forEach((id) => collect(id))
-
-        const isRemoveExistedItems =
-          itemIds.filter((id) => selectedItemIds?.includes(id)).length ===
-          itemIds.length
-
-        if (isRemoveExistedItems) {
-          const deleteIds = [...targetIds, column.parentId].filter(Boolean)
-          newIds =
-            selectedItemIds?.filter((id) => !deleteIds.includes(id)) ?? []
+        if (!isRemoveExistedItem) {
+          newIds = [
+            ...new Set([
+              ...newIds,
+              ...selectedItemIds,
+              ...collectAddParentIds(item)
+            ])
+          ]
         } else {
-          const addIds = targetIds.filter(
-            (id) => !selectedItemIds?.includes(id)
+          newIds = selectedItemIds.filter(
+            (id) => ![...newIds, ...collectRemoveParentIds(item)].includes(id)
           )
-
-          if (column.parentId) {
-            addIds.push(column.parentId)
-          }
-
-          newIds = [...(selectedItemIds ?? []), ...addIds]
         }
 
         onSelectedItemIds
@@ -200,6 +157,6 @@ export const useMillerColumns = ({
           : setSelectedItemIds(newIds)
       }
     }),
-    [columns, itemMap, activeItemId, selectedItemIds]
+    [columns, itemMap, activeItemId, disabledItemIds, selectedItemIds]
   )
 }
diff --git a/config-ui/src/components/miller-columns/hooks/use-test.ts b/config-ui/src/components/miller-columns/hooks/use-test.ts
deleted file mode 100644
index c7149f1af..000000000
--- a/config-ui/src/components/miller-columns/hooks/use-test.ts
+++ /dev/null
@@ -1,116 +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.
- *
- */
-
-import { useMemo, useState } from 'react'
-
-const items = [
-  {
-    id: 1,
-    title: 'merico-dev',
-    total: 13,
-    items: [
-      {
-        id: 11,
-        title: 'devlake'
-      },
-      {
-        id: 12,
-        title: 'devstream'
-      },
-      {
-        id: 13,
-        title: 'another-repo'
-      },
-      {
-        id: 14,
-        title: 'repo2'
-      },
-      {
-        id: 15,
-        title: 'repo2'
-      },
-      {
-        id: 16,
-        title: 'repo2'
-      },
-      {
-        id: 17,
-        title: 'repo2'
-      },
-      {
-        id: 18,
-        title: 'repo2'
-      },
-      {
-        id: 19,
-        title: 'ae-repo',
-        total: 4,
-        items: [
-          {
-            id: 191,
-            title: 'ae-repo-1'
-          },
-          {
-            id: 192,
-            title: 'ae-repo-2'
-          },
-          {
-            id: 193,
-            title: 'ae-repo-child',
-            total: 1,
-            items: [
-              {
-                id: 1931,
-                title: 'ae-repo-child-1'
-              }
-            ]
-          }
-        ]
-      }
-    ]
-  },
-  {
-    id: 2,
-    title: 'mintsweet',
-    total: 2,
-    items: [
-      {
-        id: 21,
-        title: 'reate'
-      },
-      {
-        id: 22,
-        title: 'mst-advanced'
-      }
-    ]
-  },
-  {
-    id: 3,
-    title: 'test'
-  }
-]
-
-export const useTest = () => {
-  const [ids, setIds] = useState([])
-
-  console.log(ids)
-
-  return useMemo(() => {
-    return { items, ids, setIds }
-  }, [items, ids, setIds])
-}
diff --git a/config-ui/src/components/miller-columns/index.ts b/config-ui/src/components/miller-columns/index.ts
index 07cc2c3f3..3db209c1a 100644
--- a/config-ui/src/components/miller-columns/index.ts
+++ b/config-ui/src/components/miller-columns/index.ts
@@ -17,4 +17,6 @@
  */
 
 export * from './miller-columns'
-export * from './hooks/use-test'
+export * from './components'
+export * from './hooks'
+export * from './types'
diff --git a/config-ui/src/components/miller-columns/miller-columns.tsx b/config-ui/src/components/miller-columns/miller-columns.tsx
index ce1948eb4..7aadb9a95 100644
--- a/config-ui/src/components/miller-columns/miller-columns.tsx
+++ b/config-ui/src/components/miller-columns/miller-columns.tsx
@@ -19,66 +19,50 @@
 import React from 'react'
 
 import { useMillerColumns, UseMillerColumnsProps } from './hooks'
-import { Item, ItemAll } from './components'
+import { Column, Item } from './components'
 
+import { ColumnType } from './types'
 import * as S from './styled'
 
 interface Props extends UseMillerColumnsProps {
   height?: number
   firstColumnTitle?: React.ReactNode
+  renderColumnBottom?: (col: ColumnType) => React.ReactNode
 }
 
 export const MillerColumns = ({
-  items,
-  activeItemId,
-  onActiveItemId,
-  selectedItemIds,
-  onSelectedItemIds,
   firstColumnTitle,
+  height,
+  renderColumnBottom,
   ...props
 }: Props) => {
-  const {
-    columns,
-    getStatus,
-    getChekecdStatus,
-    getCheckedCount,
-    onExpandItem,
-    onSelectItem,
-    getCheckedAllStatus,
-    onSelectAllItem
-  } = useMillerColumns({
-    items,
-    activeItemId,
-    onActiveItemId,
-    selectedItemIds,
-    onSelectedItemIds
-  })
+  const { columns, getStatus, getChekecdStatus, onExpandItem, onSelectItem } =
+    useMillerColumns(props)
 
   return (
-    <S.Container {...props}>
-      {columns.map((col, i) => (
-        <div key={col.parentId} className='items'>
-          {i === 0 && firstColumnTitle && (
-            <div className='title'>{firstColumnTitle}</div>
-          )}
-          <ItemAll
-            column={col}
-            checkStatus={getCheckedAllStatus(col)}
-            onSelectAllItem={onSelectAllItem}
+    <S.Container>
+      {columns.map((col, i) => {
+        const bottom = renderColumnBottom?.(col)
+        return (
+          <Column
+            key={col.parentId}
+            items={col.items}
+            renderItem={(item) => (
+              <Item
+                key={item.id}
+                item={item}
+                status={getStatus(item, col)}
+                checkStatus={getChekecdStatus(item)}
+                onExpandItem={onExpandItem}
+                onSelectItem={onSelectItem}
+              />
+            )}
+            height={height}
+            title={i === 0 && firstColumnTitle}
+            bottom={bottom}
           />
-          {col.items?.map((it) => (
-            <Item
-              key={it.id}
-              item={it}
-              status={getStatus(it)}
-              checkStatus={getChekecdStatus(it)}
-              checkedCount={getCheckedCount(it)}
-              onExpandItem={onExpandItem}
-              onSelectItem={onSelectItem}
-            />
-          ))}
-        </div>
-      ))}
+        )
+      })}
     </S.Container>
   )
 }
diff --git a/config-ui/src/components/miller-columns/styled.ts b/config-ui/src/components/miller-columns/styled.ts
index f22fb81d2..59f117570 100644
--- a/config-ui/src/components/miller-columns/styled.ts
+++ b/config-ui/src/components/miller-columns/styled.ts
@@ -18,29 +18,10 @@
 
 import styled from '@emotion/styled'
 
-export const Container = styled.div<{ height?: number }>`
+export const Container = styled.div`
   display: flex;
   width: 100%;
-  ${({ height }) => `height: ${height}px;`}
+  border: 1px solid #dbe4fd;
+  border-radius: 4px;
   overflow-x: auto;
-
-  .items {
-    flex: 1;
-    margin: 0;
-    padding: 0;
-    list-style: none;
-    border: 1px solid #dbe4fd;
-    border-right: none;
-    border-radius: 4px;
-
-    &:last-child {
-      border-right: 1px solid #dbe4fd;
-    }
-
-    & > .title {
-      padding: 4px 12px;
-      font-weight: 700;
-      color: #292b3f;
-    }
-  }
 `
diff --git a/config-ui/src/components/miller-columns/types.ts b/config-ui/src/components/miller-columns/types.ts
index 2624f8887..4f830f354 100644
--- a/config-ui/src/components/miller-columns/types.ts
+++ b/config-ui/src/components/miller-columns/types.ts
@@ -16,28 +16,39 @@
  *
  */
 
+export enum ItemTypeEnum {
+  LEAF = 'leaf',
+  BRANCH = 'branch'
+}
+
+export enum ItemStatusEnum {
+  PENDING = 'pending',
+  READY = 'ready'
+}
+
 export type ItemType = {
   id: string | number
   title: string
-  total?: number
-  items?: ItemType[]
+  type: ItemTypeEnum
+  status: ItemStatusEnum
+  items: ItemType[]
 }
 
 export type ItemInfoType = {
   item: ItemType
   parentId?: ItemType['id']
-  selectedChildCount: number
+  childLoaded: boolean
 }
 
 export type ItemMapType = {
   getItem: (id: ItemType['id']) => ItemType
-  getItemSelectedChildCount: (id: ItemType['id']) => number
   getItemParent: (id: ItemType['id']) => ItemType | null
+  getItemChildLoaded: (id: ItemType['id']) => boolean
 }
 
 export type ColumnType = {
   parentId: ItemType['id'] | null
-  items?: ItemType[]
+  items: ItemType[]
   activeId: ItemType['id'] | null
 }
 
diff --git a/config-ui/src/config/gitlabApiProxy.js b/config-ui/src/config/gitlabApiProxy.js
deleted file mode 100644
index a67218258..000000000
--- a/config-ui/src/config/gitlabApiProxy.js
+++ /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.
- *
- */
-// @todo: add string replacer for [:connectionId] or refactor this const
-const GITLAB_API_PROXY_ENDPOINT =
-  '/api/plugins/gitlab/connections/[:connectionId:]/proxy/rest'
-const PROJECTS_ENDPOINT = `${GITLAB_API_PROXY_ENDPOINT}/projects?search=[:search:]&membership=[:membership:]`
-
-export { GITLAB_API_PROXY_ENDPOINT, PROJECTS_ENDPOINT }
diff --git a/config-ui/src/hooks/useGitlab.jsx b/config-ui/src/hooks/useGitlab.jsx
deleted file mode 100644
index 1736c8717..000000000
--- a/config-ui/src/hooks/useGitlab.jsx
+++ /dev/null
@@ -1,100 +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.
- *
- */
-import { useEffect, useState, useCallback } from 'react'
-import request from '@/utils/request'
-import { ToastNotification } from '@/components/Toast'
-
-const useGitlab = (
-  { apiProxyPath, projectsEndpoint },
-  activeConnection = null
-) => {
-  const [isFetching, setIsFetching] = useState(false)
-  const [projects, setProjects] = useState([])
-  const [error, setError] = useState()
-
-  const fetchProjects = useCallback(
-    async (search = '', onlyQueryMemberRepo = true) => {
-      try {
-        if (apiProxyPath.includes('null')) {
-          throw new Error('Connection ID is Null')
-        }
-        setError(null)
-        setIsFetching(true)
-        if (search.length > 2) {
-          // only search when type more than 2 chars
-          const endpoint = projectsEndpoint
-            .replace('[:connectionId:]', activeConnection?.connectionId)
-            .replace('[:search:]', search)
-            .replace('[:membership:]', onlyQueryMemberRepo ? 1 : 0)
-          const projectsResponse = await request.get(endpoint)
-          if (
-            projectsResponse &&
-            projectsResponse.status === 200 &&
-            projectsResponse.data
-          ) {
-            setProjects(createListData(projectsResponse.data))
-          } else {
-            throw new Error('request projects fail')
-          }
-        } else {
-          setProjects([])
-        }
-      } catch (e) {
-        setError(e)
-        ToastNotification.show({
-          message: e.message,
-          intent: 'danger',
-          icon: 'error'
-        })
-      } finally {
-        setIsFetching(false)
-      }
-    },
-    [projectsEndpoint, activeConnection, apiProxyPath]
-  )
-
-  const createListData = (
-    data = [],
-    titleProperty = 'name_with_namespace',
-    valueProperty = 'id',
-    iconProperty = 'avatar_url'
-  ) => {
-    return data.map((d, dIdx) => ({
-      id: d[valueProperty],
-      key: d[valueProperty],
-      title: d[titleProperty],
-      shortTitle: d.name,
-      value: d[valueProperty],
-      icon: d[iconProperty],
-      type: 'string'
-    }))
-  }
-
-  useEffect(() => {
-    console.log('>>> GITLAB API PROXY: FIELD SELECTOR PROJECTS DATA', projects)
-  }, [projects])
-
-  return {
-    isFetching,
-    fetchProjects,
-    projects,
-    error
-  }
-}
-
-export default useGitlab
diff --git a/config-ui/src/models/GitlabProject.js b/config-ui/src/models/GitlabProject.js
index 91a109012..4d4bcf329 100644
--- a/config-ui/src/models/GitlabProject.js
+++ b/config-ui/src/models/GitlabProject.js
@@ -63,6 +63,7 @@ class GitlabProject extends Entity {
     this.title = data?.title || this.name || this.id || null
     this.shortTitle = data?.shortTitle || null
     this.icon = data?.icon || null
+    this.type = data?.type || null
 
     // @todo: GitLab API props to camelCase
     this.visibility = data?.visibility || 'private'
diff --git a/config-ui/src/pages/blueprints/blueprint-settings.jsx b/config-ui/src/pages/blueprints/blueprint-settings.jsx
index 3c8e00c21..0f9e3e728 100644
--- a/config-ui/src/pages/blueprints/blueprint-settings.jsx
+++ b/config-ui/src/pages/blueprints/blueprint-settings.jsx
@@ -64,11 +64,6 @@ import BlueprintDataScopesDialog from '@/components/blueprints/BlueprintDataScop
 import BlueprintNavigationLinks from '@/components/blueprints/BlueprintNavigationLinks'
 import DataScopesGrid from '@/components/blueprints/DataScopesGrid'
 import AdvancedJSON from '@/components/blueprints/create-workflow/AdvancedJSON'
-import useGitlab from '@/hooks/useGitlab'
-import {
-  GITLAB_API_PROXY_ENDPOINT,
-  PROJECTS_ENDPOINT
-} from '@/config/gitlabApiProxy'
 import useJenkins from '@/hooks/useJenkins'
 import {
   JENKINS_API_PROXY_ENDPOINT,
@@ -306,19 +301,6 @@ const BlueprintSettings = (props) => {
     configuredConnection
   )
 
-  const {
-    fetchProjects: fetchGitlabProjects,
-    projects: gitlabProjects,
-    isFetching: isFetchingGitlab,
-    error: gitlabProxyError
-  } = useGitlab(
-    {
-      apiProxyPath: GITLAB_API_PROXY_ENDPOINT,
-      projectsEndpoint: PROJECTS_ENDPOINT
-    },
-    configuredConnection
-  )
-
   const {
     fetchJobs: fetchJenkinsJobs,
     jobs: jenkinsJobs,
@@ -727,6 +709,7 @@ const BlueprintSettings = (props) => {
         break
     }
   }, [
+    skipOnFail,
     blueprintName,
     cronConfig,
     customCronConfig,
@@ -1098,7 +1081,6 @@ const BlueprintSettings = (props) => {
                         loading={
                           isFetchingBlueprint ||
                           isFetchingJIRA ||
-                          isFetchingGitlab ||
                           isFetchingJenkins
                         }
                       />
@@ -1141,7 +1123,6 @@ const BlueprintSettings = (props) => {
                       loading={
                         isFetchingBlueprint ||
                         isFetchingJIRA ||
-                        isFetchingGitlab ||
                         isFetchingJenkins
                       }
                     />
@@ -1268,10 +1249,6 @@ const BlueprintSettings = (props) => {
         fieldsList={jiraApiFields}
         isFetching={isFetchingBlueprint}
         isFetchingJIRA={isFetchingJIRA}
-        fetchGitlabProjects={fetchGitlabProjects}
-        gitlabProjects={gitlabProjects}
-        isFetchingGitlab={isFetchingGitlab}
-        gitlabProxyError={gitlabProxyError}
         fetchJenkinsJobs={fetchJenkinsJobs}
         jenkinsJobs={jenkinsJobs}
         isFetchingJenkins={isFetchingJenkins}
diff --git a/config-ui/src/pages/blueprints/create-blueprint.jsx b/config-ui/src/pages/blueprints/create-blueprint.jsx
index 0a20f34bf..adadea672 100644
--- a/config-ui/src/pages/blueprints/create-blueprint.jsx
+++ b/config-ui/src/pages/blueprints/create-blueprint.jsx
@@ -39,10 +39,6 @@ import { WorkflowAdvancedSteps, WorkflowSteps } from '@/data/BlueprintWorkflow'
 
 import { DEVLAKE_ENDPOINT } from '@/utils/config'
 import request from '@/utils/request'
-import {
-  GITLAB_API_PROXY_ENDPOINT,
-  PROJECTS_ENDPOINT
-} from '@/config/gitlabApiProxy'
 import {
   JENKINS_API_PROXY_ENDPOINT,
   JENKINS_JOBS_ENDPOINT
@@ -58,7 +54,6 @@ import usePipelineValidation from '@/hooks/usePipelineValidation'
 import useConnectionValidation from '@/hooks/useConnectionValidation'
 import useJIRA from '@/hooks/useJIRA'
 import useJenkins from '@/hooks/useJenkins'
-import useGitlab from '@/hooks/useGitlab'
 
 import WorkflowStepsBar from '@/components/blueprints/WorkflowStepsBar'
 import WorkflowActions from '@/components/blueprints/WorkflowActions'
@@ -284,19 +279,6 @@ const CreateBlueprint = (props) => {
     configuredConnection
   )
 
-  const {
-    fetchProjects: fetchGitlabProjects,
-    projects: gitlabProjects,
-    isFetching: isFetchingGitlab,
-    error: gitlabProxyError
-  } = useGitlab(
-    {
-      apiProxyPath: GITLAB_API_PROXY_ENDPOINT,
-      projectsEndpoint: PROJECTS_ENDPOINT
-    },
-    configuredConnection
-  )
-
   const {
     fetchJobs: fetchJenkinsJobs,
     jobs: jenkinsJobs,
@@ -978,9 +960,6 @@ const CreateBlueprint = (props) => {
                       activeConnectionTab={activeConnectionTab}
                       blueprintConnections={blueprintConnections}
                       jiraBoards={jiraBoards}
-                      fetchGitlabProjects={fetchGitlabProjects}
-                      isFetchingGitlab={isFetchingGitlab}
-                      gitlabProjects={gitlabProjects}
                       fetchJenkinsJobs={fetchJenkinsJobs}
                       isFetchingJenkins={isFetchingJenkins}
                       jenkinsJobs={jenkinsJobs}
@@ -1000,7 +979,6 @@ const CreateBlueprint = (props) => {
                       ]}
                       isFetching={
                         isFetchingJIRA ||
-                        isFetchingGitlab ||
                         isFetchingJenkins ||
                         isFetchingConnection
                       }
@@ -1080,7 +1058,6 @@ const CreateBlueprint = (props) => {
               isLoading={
                 isSaving ||
                 isFetchingJIRA ||
-                isFetchingGitlab ||
                 isFetchingJenkins ||
                 isFetchingConnection ||
                 isTestingConnection
diff --git a/config-ui/tsconfig.json b/config-ui/tsconfig.json
index 9f8365997..fa0074186 100644
--- a/config-ui/tsconfig.json
+++ b/config-ui/tsconfig.json
@@ -1,5 +1,9 @@
 {
   "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    },
     "target": "ESNext",
     "useDefineForClassFields": true,
     "lib": ["DOM", "DOM.Iterable", "ESNext"],