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 10f3308b feat(config-ui): added component miller-columns (#3683)
10f3308b is described below
commit 10f3308bdb208e95ea4c9467e24f4757207f13ff
Author: 青湛 <[email protected]>
AuthorDate: Wed Nov 9 16:06:08 2022 +0800
feat(config-ui): added component miller-columns (#3683)
---
config-ui/src/App.js | 12 ++
.../components/checkbox/checkbox.tsx | 45 +++++
.../miller-columns/components/checkbox/index.ts | 20 ++
.../miller-columns/components/checkbox/styled.ts | 104 +++++++++++
.../miller-columns/components/checkbox/types.ts | 23 +++
.../components/miller-columns/components/index.ts | 20 ++
.../miller-columns/components/item/index.ts | 20 ++
.../miller-columns/components/item/item-all.tsx | 45 +++++
.../miller-columns/components/item/item.tsx | 67 +++++++
.../miller-columns/components/item/styled.ts | 40 ++++
.../src/components/miller-columns/hooks/index.ts | 22 +++
.../components/miller-columns/hooks/use-columns.ts | 65 +++++++
.../miller-columns/hooks/use-item-map.ts | 79 ++++++++
.../miller-columns/hooks/use-miller-columns.ts | 205 +++++++++++++++++++++
.../components/miller-columns/hooks/use-test.ts | 116 ++++++++++++
config-ui/src/components/miller-columns/index.ts | 20 ++
.../components/miller-columns/miller-columns.tsx | 84 +++++++++
config-ui/src/components/miller-columns/styled.ts | 46 +++++
config-ui/src/components/miller-columns/types.ts | 53 ++++++
19 files changed, 1086 insertions(+)
diff --git a/config-ui/src/App.js b/config-ui/src/App.js
index 1fdf9447..c4729c45 100644
--- a/config-ui/src/App.js
+++ b/config-ui/src/App.js
@@ -42,6 +42,7 @@ 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 {
@@ -55,6 +56,8 @@ function App(props) {
handleMigrationDialogClose
} = useDatabaseMigrations()
+ const { items, ids, setIds } = useTest()
+
return (
<Router>
<Route exact path='/'>
@@ -105,6 +108,15 @@ 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/miller-columns/components/checkbox/checkbox.tsx
b/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
new file mode 100644
index 00000000..a3394e58
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/checkbox.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 classNames from 'classnames'
+
+import { CheckStatus } from './types'
+import * as S from './styled'
+
+interface Props {
+ status?: CheckStatus
+ children?: React.ReactNode
+ onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+}
+
+export const Checkbox = ({ children, status, onClick }: Props) => {
+ const checkboxCls = classNames('checkbox', {
+ 'checkbox-checked': status === CheckStatus.checked,
+ 'checkbox-indeterminate': status === CheckStatus.indeterminate
+ })
+
+ return (
+ <S.Wrapper>
+ <span className={checkboxCls} onClick={onClick}>
+ <span className='checkbox-inner'></span>
+ </span>
+ {children && <span className='text'>{children}</span>}
+ </S.Wrapper>
+ )
+}
diff --git
a/config-ui/src/components/miller-columns/components/checkbox/index.ts
b/config-ui/src/components/miller-columns/components/checkbox/index.ts
new file mode 100644
index 00000000..4941e126
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './checkbox'
+export * from './types'
diff --git
a/config-ui/src/components/miller-columns/components/checkbox/styled.ts
b/config-ui/src/components/miller-columns/components/checkbox/styled.ts
new file mode 100644
index 00000000..1eda3718
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/styled.ts
@@ -0,0 +1,104 @@
+/*
+ * 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 styled from '@emotion/styled'
+
+export const Wrapper = styled.label`
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+
+ .checkbox {
+ position: relative;
+ margin-right: 8px;
+
+ &.checkbox-checked {
+ .checkbox-inner {
+ background-color: #7497f7;
+ border-color: #7497f7;
+
+ &::after {
+ content: ' ';
+ transform: rotate(45deg) scale(1) translate(-50%, -50%);
+ opacity: 1;
+ transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
+ }
+ }
+ }
+
+ &.checkbox-indeterminate {
+ .checkbox-inner {
+ background-color: #fff;
+ border-color: #d9d9d9;
+
+ &::after {
+ content: ' ';
+ left: 50%;
+ width: 8px;
+ height: 8px;
+ background-color: #7497f7;
+ border: 0;
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 1;
+ }
+ }
+ }
+
+ .checkbox-input {
+ position: absolute;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ opacity: 0;
+ }
+
+ .checkbox-inner {
+ position: relative;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 14px;
+ height: 14px;
+ background-color: #fff;
+ border: 1px solid #70727f;
+ border-radius: 2px;
+ transition: all 0.3s;
+
+ &::after {
+ content: ' ';
+ position: absolute;
+ top: 50%;
+ left: 21.5%;
+ display: table;
+ width: 5.71428571px;
+ height: 9.14285714px;
+ border: 2px solid #fff;
+ border-top: 0;
+ border-left: 0;
+ transform: rotate(45deg) scale(0) translate(-50%, -50%);
+ opacity: 0;
+ transition: all 0.1s cubic-bezier(0.71, -0.46, 0.88, 0.6), opacity
0.1s;
+ }
+ }
+ }
+
+ .text {
+ font-size: 14px;
+ }
+`
diff --git
a/config-ui/src/components/miller-columns/components/checkbox/types.ts
b/config-ui/src/components/miller-columns/components/checkbox/types.ts
new file mode 100644
index 00000000..eb33ae2d
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/checkbox/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export enum CheckStatus {
+ nochecked = 'nochecked',
+ checked = 'checked',
+ indeterminate = 'indeterminate'
+}
diff --git a/config-ui/src/components/miller-columns/components/index.ts
b/config-ui/src/components/miller-columns/components/index.ts
new file mode 100644
index 00000000..25c3b950
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './checkbox'
+export * from './item'
diff --git a/config-ui/src/components/miller-columns/components/item/index.ts
b/config-ui/src/components/miller-columns/components/item/index.ts
new file mode 100644
index 00000000..d8175878
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './item'
+export * from './item-all'
diff --git
a/config-ui/src/components/miller-columns/components/item/item-all.tsx
b/config-ui/src/components/miller-columns/components/item/item-all.tsx
new file mode 100644
index 00000000..a0dc0cb9
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/item-all.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 type { ColumnType } from '../../types'
+
+import { Checkbox, CheckStatus } from '../checkbox'
+
+import * as S from './styled'
+
+interface Props {
+ column: ColumnType
+ checkStatus?: CheckStatus
+ onSelectAllItem?: (column: ColumnType) => void
+}
+
+export const ItemAll = ({ column, checkStatus, onSelectAllItem }: Props) => {
+ const handleCheckboxClick = (e: React.MouseEvent<HTMLDivElement>) => {
+ e.stopPropagation()
+ onSelectAllItem?.(column)
+ }
+
+ return (
+ <S.Wrapper selected={false}>
+ <Checkbox status={checkStatus} onClick={handleCheckboxClick} />
+ <span>All</span>
+ </S.Wrapper>
+ )
+}
diff --git a/config-ui/src/components/miller-columns/components/item/item.tsx
b/config-ui/src/components/miller-columns/components/item/item.tsx
new file mode 100644
index 00000000..ca71d17c
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/item.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { ItemType, RowStatus } from '../../types'
+
+import { Checkbox, CheckStatus } from '../checkbox'
+
+import * as S from './styled'
+
+interface Props {
+ item: ItemType
+ status?: RowStatus
+ checkStatus?: CheckStatus
+ checkedCount?: number
+ onExpandItem?: (it: ItemType) => void
+ onSelectItem?: (it: ItemType) => void
+}
+
+export const Item = ({
+ item,
+ status = RowStatus.noselected,
+ checkStatus = CheckStatus.nochecked,
+ checkedCount = 0,
+ onExpandItem,
+ onSelectItem
+}: Props) => {
+ const handleRowClick = () => {
+ onExpandItem?.(item)
+ }
+
+ const handleCheckboxClick = (e: React.MouseEvent<HTMLDivElement>) => {
+ e.stopPropagation()
+ onSelectItem?.(item)
+ }
+
+ return (
+ <S.Wrapper
+ 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>
+ )}
+ </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
new file mode 100644
index 00000000..7c2dab4b
--- /dev/null
+++ b/config-ui/src/components/miller-columns/components/item/styled.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 styled from '@emotion/styled'
+
+export const Wrapper = styled.div<{ selected: boolean }>`
+ display: flex;
+ align-items: center;
+ padding: 4px 12px;
+ cursor: pointer;
+
+ ${({ selected }) => (selected ? 'background-color: #f5f5f7;' : '')}
+
+ &:hover {
+ background-color: #f5f5f7;
+ }
+
+ & > span.name {
+ font-size: 14px;
+ }
+
+ & > span.count {
+ font-size: 14px;
+ }
+`
diff --git a/config-ui/src/components/miller-columns/hooks/index.ts
b/config-ui/src/components/miller-columns/hooks/index.ts
new file mode 100644
index 00000000..d9c9dbfb
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './use-columns'
+export * from './use-item-map'
+export * from './use-miller-columns'
+export * from './use-test'
diff --git a/config-ui/src/components/miller-columns/hooks/use-columns.ts
b/config-ui/src/components/miller-columns/hooks/use-columns.ts
new file mode 100644
index 00000000..9753e2f7
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-columns.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 } from 'react'
+
+import { ItemType, ItemMapType, ColumnType } from '../types'
+
+interface Props {
+ items: ItemType[]
+ itemMap: ItemMapType
+ activeItemId?: ItemType['id']
+}
+
+export const useColumns = ({ items, itemMap, activeItemId }: Props) => {
+ return useMemo(() => {
+ const rootLeaf = { items, activeId: null, parentId: null }
+
+ if (!activeItemId) {
+ return [rootLeaf]
+ }
+
+ const activeItem = itemMap.getItem(activeItemId)
+
+ const columns: ColumnType[] = [
+ {
+ parentId: activeItem.id,
+ items: activeItem.items,
+ activeId: null
+ }
+ ]
+
+ const collect = (item: ItemType) => {
+ const parent = itemMap.getItemParent(item.id)
+
+ columns.unshift({
+ parentId: parent?.id ?? null,
+ items: parent?.items ?? items,
+ activeId: item.id ?? null
+ })
+
+ if (parent) {
+ collect(parent)
+ }
+ }
+
+ collect(activeItem)
+
+ return columns
+ }, [items, itemMap, activeItemId])
+}
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
new file mode 100644
index 00000000..ad62205c
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-item-map.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 } from 'react'
+
+import type { ItemType, ItemInfoType } from '../types'
+
+interface Props {
+ items: ItemType[]
+ selectedItemIds?: Array<ItemType['id']>
+}
+
+export const useItemMap = ({ items, selectedItemIds = [] }: Props) => {
+ return useMemo(() => {
+ const itemMap = new Map<ItemType['id'], ItemInfoType>()
+
+ const collect = ({
+ item,
+ parent
+ }: {
+ item: ItemType
+ parent?: ItemType
+ }) => {
+ if (!itemMap.has(item.id)) {
+ itemMap.set(item.id, {
+ item,
+ parentId: parent?.id,
+ selectedChildCount: 0
+ })
+ }
+
+ if (item.items) {
+ item.items.forEach((it) => collect({ item: it, parent: item }))
+ }
+ }
+
+ 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
+ }
+ }
+ }, [items, selectedItemIds])
+}
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
new file mode 100644
index 00000000..837f437c
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-miller-columns.ts
@@ -0,0 +1,205 @@
+/*
+ * 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, useEffect } from 'react'
+
+import type { ItemType, ColumnType } from '../types'
+import { RowStatus } from '../types'
+import { CheckStatus } from '../components'
+
+import { useItemMap } from './use-item-map'
+import { useColumns } from './use-columns'
+
+export interface UseMillerColumnsProps {
+ items: ItemType[]
+ activeItemId?: ItemType['id']
+ onActiveItemId?: (id: ItemType['id']) => void
+ selectedItemIds?: Array<ItemType['id']>
+ onSelectedItemIds?: (ids: Array<ItemType['id']>) => void
+}
+
+export const useMillerColumns = ({
+ items,
+ onActiveItemId,
+ onSelectedItemIds,
+ ...props
+}: UseMillerColumnsProps) => {
+ const [activeItemId, setActiveItemId] = useState<ItemType['id']>()
+ const [selectedItemIds, setSelectedItemIds] =
+ useState<Array<ItemType['id']>>()
+
+ const itemMap = useItemMap({ items, selectedItemIds })
+ const columns = useColumns({ items, itemMap, activeItemId })
+
+ useEffect(() => {
+ setActiveItemId(props.activeItemId)
+ }, [props.activeItemId])
+
+ useEffect(() => {
+ setSelectedItemIds(props.selectedItemIds)
+ }, [props.selectedItemIds])
+
+ return useMemo(
+ () => ({
+ columns,
+ itemMap,
+ activeItemId,
+ selectedItemIds,
+ getStatus(item: ItemType) {
+ if (item.id === activeItemId) {
+ return RowStatus.selected
+ }
+ return RowStatus.noselected
+ },
+ getChekecdStatus(item: ItemType) {
+ if (!selectedItemIds?.length) {
+ return CheckStatus.nochecked
+ }
+ if (selectedItemIds?.includes(item.id)) {
+ return CheckStatus.checked
+ }
+
+ 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:
+ return CheckStatus.checked
+ case !!colSelectedIds.length:
+ return CheckStatus.indeterminate
+ default:
+ return CheckStatus.nochecked
+ }
+ },
+ getCheckedCount(item: ItemType) {
+ return itemMap.getItemSelectedChildCount(item.id)
+ },
+ onExpandItem(item: ItemType) {
+ if (!item.items?.length) {
+ return
+ }
+ 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)
+ }
+ }
+
+ newIds = [...(selectedItemIds ?? []), ...addIds]
+ }
+
+ 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))
+ }
+ }
+
+ 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)) ?? []
+ } else {
+ const addIds = targetIds.filter(
+ (id) => !selectedItemIds?.includes(id)
+ )
+
+ if (column.parentId) {
+ addIds.push(column.parentId)
+ }
+
+ newIds = [...(selectedItemIds ?? []), ...addIds]
+ }
+
+ onSelectedItemIds
+ ? onSelectedItemIds(newIds)
+ : setSelectedItemIds(newIds)
+ }
+ }),
+ [columns, itemMap, activeItemId, 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
new file mode 100644
index 00000000..c7149f1a
--- /dev/null
+++ b/config-ui/src/components/miller-columns/hooks/use-test.ts
@@ -0,0 +1,116 @@
+/*
+ * 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
new file mode 100644
index 00000000..07cc2c3f
--- /dev/null
+++ b/config-ui/src/components/miller-columns/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './miller-columns'
+export * from './hooks/use-test'
diff --git a/config-ui/src/components/miller-columns/miller-columns.tsx
b/config-ui/src/components/miller-columns/miller-columns.tsx
new file mode 100644
index 00000000..ce1948eb
--- /dev/null
+++ b/config-ui/src/components/miller-columns/miller-columns.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { useMillerColumns, UseMillerColumnsProps } from './hooks'
+import { Item, ItemAll } from './components'
+
+import * as S from './styled'
+
+interface Props extends UseMillerColumnsProps {
+ height?: number
+ firstColumnTitle?: React.ReactNode
+}
+
+export const MillerColumns = ({
+ items,
+ activeItemId,
+ onActiveItemId,
+ selectedItemIds,
+ onSelectedItemIds,
+ firstColumnTitle,
+ ...props
+}: Props) => {
+ const {
+ columns,
+ getStatus,
+ getChekecdStatus,
+ getCheckedCount,
+ onExpandItem,
+ onSelectItem,
+ getCheckedAllStatus,
+ onSelectAllItem
+ } = useMillerColumns({
+ items,
+ activeItemId,
+ onActiveItemId,
+ selectedItemIds,
+ onSelectedItemIds
+ })
+
+ 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}
+ />
+ {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
new file mode 100644
index 00000000..f22fb81d
--- /dev/null
+++ b/config-ui/src/components/miller-columns/styled.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 styled from '@emotion/styled'
+
+export const Container = styled.div<{ height?: number }>`
+ display: flex;
+ width: 100%;
+ ${({ height }) => `height: ${height}px;`}
+ 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
new file mode 100644
index 00000000..2624f888
--- /dev/null
+++ b/config-ui/src/components/miller-columns/types.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export type ItemType = {
+ id: string | number
+ title: string
+ total?: number
+ items?: ItemType[]
+}
+
+export type ItemInfoType = {
+ item: ItemType
+ parentId?: ItemType['id']
+ selectedChildCount: number
+}
+
+export type ItemMapType = {
+ getItem: (id: ItemType['id']) => ItemType
+ getItemSelectedChildCount: (id: ItemType['id']) => number
+ getItemParent: (id: ItemType['id']) => ItemType | null
+}
+
+export type ColumnType = {
+ parentId: ItemType['id'] | null
+ items?: ItemType[]
+ activeId: ItemType['id'] | null
+}
+
+export enum RowStatus {
+ selected = 'selected',
+ noselected = 'noselected'
+}
+
+export enum CheckedStatus {
+ selected = 'selected',
+ noselected = 'noselected',
+ indeterminate = 'indeterminate'
+}