From 21a78f14fbeca842aa3a160dfe333f269aa66c33 Mon Sep 17 00:00:00 2001 From: amlannandy Date: Sun, 22 Dec 2024 18:16:16 +0530 Subject: [PATCH] feat: add group by and quick filters --- .../api/infraMonitoring/getK8sClustersList.ts | 13 +- .../Clusters/K8sClustersList.tsx | 366 +++++++++++++++--- .../InfraMonitoringK8s/Clusters/utils.tsx | 229 ++++------- .../InfraMonitoringK8s/InfraMonitoringK8s.tsx | 6 +- .../container/InfraMonitoringK8s/constants.ts | 23 +- 5 files changed, 401 insertions(+), 236 deletions(-) diff --git a/frontend/src/api/infraMonitoring/getK8sClustersList.ts b/frontend/src/api/infraMonitoring/getK8sClustersList.ts index 93ec902fae2..f7996765688 100644 --- a/frontend/src/api/infraMonitoring/getK8sClustersList.ts +++ b/frontend/src/api/infraMonitoring/getK8sClustersList.ts @@ -17,19 +17,14 @@ export interface K8sClustersListPayload { } export interface K8sClustersData { - clusterName: string; + clusterUID: string; cpuUsage: number; + cpuAllocatable: number; memoryUsage: number; - desiredPods: number; - availablePods: number; - cpuRequest: number; - memoryRequest: number; - cpuLimit: number; - memoryLimit: number; - restarts: number; + memoryAllocatable: number; meta: { k8s_cluster_name: string; - k8s_namespace_name: string; + k8s_cluster_uid: string; }; } diff --git a/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx b/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx index 65a46240666..f5d72fbb11f 100644 --- a/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx @@ -1,19 +1,24 @@ +/* eslint-disable no-restricted-syntax */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import '../InfraMonitoringK8s.styles.scss'; import { LoadingOutlined } from '@ant-design/icons'; import { - Skeleton, + Button, Spin, Table, TablePaginationConfig, TableProps, Typography, } from 'antd'; -import { SorterResult } from 'antd/es/table/interface'; +import { ColumnType, SorterResult } from 'antd/es/table/interface'; import logEvent from 'api/common/logEvent'; import { K8sClustersListPayload } from 'api/infraMonitoring/getK8sClustersList'; import { useGetK8sClustersList } from 'hooks/infraMonitoring/useGetK8sClustersList'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; +import { ChevronDown, ChevronRight } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -21,6 +26,8 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import K8sHeader from '../K8sHeader'; +import LoadingContainer from '../LoadingContainer'; +import { dummyColumnConfig } from '../utils'; import { defaultAddedColumns, formatDataForTable, @@ -42,10 +49,7 @@ function K8sClustersList({ const [currentPage, setCurrentPage] = useState(1); - const [filters, setFilters] = useState({ - items: [], - op: 'and', - }); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [orderBy, setOrderBy] = useState<{ columnName: string; @@ -56,35 +60,158 @@ function K8sClustersList({ const pageSize = 10; - const query = useMemo(() => { + const [groupBy, setGroupBy] = useState([]); + + const [ + selectedRowData, + setSelectedRowData, + ] = useState(null); + + const [groupByOptions, setGroupByOptions] = useState< + { value: string; label: string }[] + >([]); + + const createFiltersForSelectedRowData = ( + selectedRowData: K8sClustersRowData, + ): IBuilderQuery['filters'] => { + const baseFilters: IBuilderQuery['filters'] = { + items: [], + op: 'and', + }; + + if (!selectedRowData) return baseFilters; + + const { groupedByMeta } = selectedRowData; + + for (const key of Object.keys(groupedByMeta)) { + baseFilters.items.push({ + key: { + key, + }, + op: '=', + value: groupedByMeta[key], + }); + } + + return baseFilters; + }; + + const fetchGroupedByRowDataQuery = useMemo(() => { + if (!selectedRowData) return null; + const baseQuery = getK8sClustersListQuery(); + + const filters = createFiltersForSelectedRowData(selectedRowData); + return { + ...baseQuery, + limit: 10, + offset: 0, + filters, + start: Math.floor(minTime / 1000000), + end: Math.floor(maxTime / 1000000), + orderBy, + }; + }, [minTime, maxTime, orderBy, selectedRowData]); + + const { + data: groupedByRowData, + isFetching: isFetchingGroupedByRowData, + isLoading: isLoadingGroupedByRowData, + isError: isErrorGroupedByRowData, + refetch: fetchGroupedByRowData, + } = useGetK8sClustersList( + fetchGroupedByRowDataQuery as K8sClustersListPayload, + { + queryKey: ['clusterList', fetchGroupedByRowDataQuery], + enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, + }, + ); + + const { currentQuery } = useQueryBuilder(); + + const { + data: groupByFiltersData, + isLoading: isLoadingGroupByFilters, + } = useGetAggregateKeys( + { + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateAttribute: '', + aggregateOperator: 'noop', + searchText: '', + tagType: '', + }, + { + queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'], + }, + true, + ); + + const queryFilters = useMemo( + () => + currentQuery?.builder?.queryData[0]?.filters || { + items: [], + op: 'and', + }, + [currentQuery?.builder?.queryData], + ); + + const query = useMemo(() => { + const baseQuery = getK8sClustersListQuery(); + const queryPayload = { ...baseQuery, limit: pageSize, offset: (currentPage - 1) * pageSize, - filters, + filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), orderBy, }; - }, [currentPage, filters, minTime, maxTime, orderBy]); + if (groupBy.length > 0) { + queryPayload.groupBy = groupBy; + } + return queryPayload; + }, [currentPage, minTime, maxTime, orderBy, groupBy, queryFilters]); + + const formattedGroupedByClustersData = useMemo( + () => + formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy), + [groupedByRowData, groupBy], + ); const { data, isFetching, isLoading, isError } = useGetK8sClustersList( query as K8sClustersListPayload, { - queryKey: ['hostList', query], + queryKey: ['clusterList', query], enabled: !!query, }, ); - const ClustersData = useMemo(() => data?.payload?.data?.records || [], [data]); + const clustersData = useMemo(() => data?.payload?.data?.records || [], [data]); const totalCount = data?.payload?.data?.total || 0; - const formattedClustersData = useMemo(() => formatDataForTable(ClustersData), [ - ClustersData, - ]); + const formattedClustersData = useMemo( + () => formatDataForTable(clustersData, groupBy), + [clustersData, groupBy], + ); - const columns = useMemo(() => getK8sClustersListColumns(), []); + const columns = useMemo(() => getK8sClustersListColumns(groupBy), [groupBy]); + + const handleGroupByRowClick = (record: K8sClustersRowData): void => { + setSelectedRowData(record); + + if (expandedRowKeys.includes(record.key)) { + setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key)); + } else { + setExpandedRowKeys([record.key]); + } + }; + + useEffect(() => { + if (selectedRowData) { + fetchGroupedByRowData(); + } + }, [selectedRowData, fetchGroupedByRowData]); const handleTableChange: TableProps['onChange'] = useCallback( ( @@ -110,19 +237,22 @@ function K8sClustersList({ [], ); + const { handleChangeQueryData } = useQueryOperations({ + index: 0, + query: currentQuery.builder.queryData[0], + entityVersion: '', + }); + const handleFiltersChange = useCallback( (value: IBuilderQuery['filters']): void => { - const isNewFilterAdded = value.items.length !== filters.items.length; - if (isNewFilterAdded) { - setFilters(value); - setCurrentPage(1); + handleChangeQueryData('filters', value); + setCurrentPage(1); - logEvent('Infra Monitoring: K8s list filters applied', { - filters: value, - }); - } + logEvent('Infra Monitoring: K8s list filters applied', { + filters: value, + }); }, - [filters], + [handleChangeQueryData], ); useEffect(() => { @@ -131,17 +261,121 @@ function K8sClustersList({ // const selectedClusterData = useMemo(() => { // if (!selectedClusterUID) return null; - // return ClustersData.find((cluster) => cluster.ClusterUID === selectedClusterUID) || null; - // }, [selectedClusterUID, ClustersData]); + // return clustersData.find((cluster) => cluster.clusterUID === selectedClusterUID) || null; + // }, [selectedClusterUID, clustersData]); const handleRowClick = (record: K8sClustersRowData): void => { - // setselectedClusterUID(record.ClusterUID); + if (groupBy.length === 0) { + setSelectedRowData(null); + // setselectedClusterUID(record.clusterUID); + } else { + handleGroupByRowClick(record); + } logEvent('Infra Monitoring: K8s cluster list item clicked', { - clusterName: record.clusterName, + clusterUID: record.clusterName, }); }; + const nestedColumns = useMemo(() => { + const nestedColumns = getK8sClustersListColumns([]); + return [dummyColumnConfig, ...nestedColumns]; + }, []); + + const isGroupedByAttribute = groupBy.length > 0; + + const handleExpandedRowViewAllClick = (): void => { + if (!selectedRowData) return; + + const filters = createFiltersForSelectedRowData(selectedRowData); + + handleFiltersChange(filters); + + setCurrentPage(1); + setSelectedRowData(null); + setGroupBy([]); + setOrderBy(null); + }; + + const expandedRowRender = (): JSX.Element => ( +
+ {isErrorGroupedByRowData && ( + {groupedByRowData?.error || 'Something went wrong'} + )} + {isFetchingGroupedByRowData || isLoadingGroupedByRowData ? ( + + ) : ( +
+ []} + dataSource={formattedGroupedByClustersData} + pagination={false} + scroll={{ x: true }} + tableLayout="fixed" + size="small" + loading={{ + spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData, + indicator: } />, + }} + /> + + {groupedByRowData?.payload?.data?.total && + groupedByRowData?.payload?.data?.total > 10 ? ( +
+ +
+ ) : null} + + )} + + ); + + const expandRowIconRenderer = ({ + expanded, + onExpand, + record, + }: { + expanded: boolean; + onExpand: ( + record: K8sClustersRowData, + e: React.MouseEvent, + ) => void; + record: K8sClustersRowData; + }): JSX.Element | null => { + if (!isGroupedByAttribute) { + return null; + } + + return expanded ? ( + + ) : ( + + ); + }; + // const handleCloseClusterDetail = (): void => { // setselectedClusterUID(null); // }; @@ -150,13 +384,46 @@ function K8sClustersList({ !isError && !isLoading && !isFetching && - !(formattedClustersData.length === 0 && filters.items.length > 0); + !(formattedClustersData.length === 0 && queryFilters.items.length > 0); const showNoFilteredClustersMessage = !isFetching && !isLoading && formattedClustersData.length === 0 && - filters.items.length > 0; + queryFilters.items.length > 0; + + const handleGroupByChange = useCallback( + (value: IBuilderQuery['groupBy']) => { + const groupBy = []; + + for (let index = 0; index < value.length; index++) { + const element = (value[index] as unknown) as string; + + const key = groupByFiltersData?.payload?.attributeKeys?.find( + (key) => key.key === element, + ); + + if (key) { + groupBy.push(key); + } + } + + setGroupBy(groupBy); + setExpandedRowKeys([]); + }, + [groupByFiltersData], + ); + + useEffect(() => { + if (groupByFiltersData?.payload) { + setGroupByOptions( + groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({ + value: filter.key, + label: filter.key, + })) || [], + ); + } + }, [groupByFiltersData]); return (
@@ -164,16 +431,16 @@ function K8sClustersList({ isFiltersVisible={isFiltersVisible} handleFilterVisibilityChange={handleFilterVisibilityChange} defaultAddedColumns={defaultAddedColumns} - addedColumns={[]} - availableColumns={[]} handleFiltersChange={handleFiltersChange} - onAddColumn={() => {}} - onRemoveColumn={() => {}} + groupByOptions={groupByOptions} + isLoadingGroupByFilters={isLoadingGroupByFilters} + handleGroupByChange={handleGroupByChange} + selectedGroupBy={groupBy} /> {isError && {data?.error || 'Something went wrong'}} {showNoFilteredClustersMessage && ( -
+
)} - {(isFetching || isLoading) && ( -
- - - -
- )} + {(isFetching || isLoading) && } {showsClustersTable && (
} />, }} tableLayout="fixed" - rowKey={(record): string => record.clusterName} onChange={handleTableChange} onRow={(record): { onClick: () => void; className: string } => ({ onClick: (): void => handleRowClick(record), className: 'clickable-row', })} + expandable={{ + expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined, + expandIcon: expandRowIconRenderer, + expandedRowKeys, + }} /> )} {/* TODO - Handle Cluster Details flow */} diff --git a/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx index c398a0812ad..818e0165c04 100644 --- a/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx @@ -1,18 +1,19 @@ -import { Color } from '@signozhq/design-tokens'; -import { Progress } from 'antd'; +import { Tag } from 'antd'; import { ColumnType } from 'antd/es/table'; import { K8sClustersData, K8sClustersListPayload, } from 'api/infraMonitoring/getK8sClustersList'; +import { Group } from 'lucide-react'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IEntityColumn } from '../utils'; export const defaultAddedColumns: IEntityColumn[] = [ { - label: 'Namespace Status', - value: 'NamespaceStatus', - id: 'NamespaceStatus', + label: 'Cluster Name', + value: 'clusterName', + id: 'cluster', canRemove: false, }, { @@ -28,33 +29,44 @@ export const defaultAddedColumns: IEntityColumn[] = [ canRemove: false, }, { - label: 'Memory Allocatable (bytes)', - value: 'memoryAllocatable', - id: 'memoryAllocatable', + label: 'Memory Utilization (cores)', + value: 'memoryUsage', + id: 'memoryUsage', canRemove: false, }, { - label: 'Pods count by phase', - value: 'podsCount', - id: 'podsCount', + label: 'Memory Allocatable (bytes)', + value: 'memoryAllocatable', + id: 'memoryAllocatable', canRemove: false, }, ]; export interface K8sClustersRowData { key: string; + clusterUID: string; clusterName: string; - availableReplicas: number; - desiredReplicas: number; - cpuRequestUtilization: React.ReactNode; - cpuLimitUtilization: React.ReactNode; cpuUtilization: number; - memoryRequestUtilization: React.ReactNode; - memoryLimitUtilization: React.ReactNode; memoryUtilization: number; - clusterRestarts: number; + cpuAllocatable: number; + memoryAllocatable: number; + groupedByMeta?: any; } +const clusterGroupColumnConfig = { + title: ( +
+ CLUSTER GROUP +
+ ), + dataIndex: 'clusterGroup', + key: 'clusterGroup', + ellipsis: true, + width: 150, + align: 'left', + sorter: false, +}; + export const getK8sClustersListQuery = (): K8sClustersListPayload => ({ filters: { items: [], @@ -65,7 +77,7 @@ export const getK8sClustersListQuery = (): K8sClustersListPayload => ({ const columnsConfig = [ { - title:
Cluster
, + title:
Cluster Name
, dataIndex: 'clusterName', key: 'clusterName', ellipsis: true, @@ -73,46 +85,6 @@ const columnsConfig = [ sorter: true, align: 'left', }, - { - title:
Available Replicas
, - dataIndex: 'availableReplicas', - key: 'availableReplicas', - width: 100, - sorter: true, - align: 'left', - }, - { - title:
Desired Replicas
, - dataIndex: 'desiredReplicas', - key: 'desiredReplicas', - width: 80, - sorter: true, - align: 'left', - }, - { - title: ( -
- CPU Request Utilization (% of limit) -
- ), - dataIndex: 'cpuRequestUtilization', - key: 'cpuRequestUtilization', - width: 80, - sorter: true, - align: 'left', - }, - { - title: ( -
- CPU Limit Utilization (% of request) -
- ), - dataIndex: 'cpuLimitUtilization', - key: 'cpuLimitUtilization', - width: 50, - sorter: true, - align: 'left', - }, { title:
CPU Utilization (cores)
, dataIndex: 'cpuUtilization', @@ -122,117 +94,80 @@ const columnsConfig = [ align: 'left', }, { - title: ( -
- Memory Request Utilization (% of limit) -
- ), - dataIndex: 'memoryRequestUtilization', - key: 'memoryRequestUtilization', - width: 50, + title:
CPU Allocatable (cores)
, + dataIndex: 'cpuAllocatable', + key: 'cpuAllocatable', + width: 80, sorter: true, align: 'left', }, { - title: ( -
- Memory Limit Utilization (% of request) -
- ), - dataIndex: 'memoryLimitUtilization', - key: 'memoryLimitUtilization', + title:
Memory Utilization (bytes)
, + dataIndex: 'memoryUtilization', + key: 'memoryUtilization', width: 80, sorter: true, align: 'left', }, { - title:
Cluster Restarts
, - dataIndex: 'clusterRestarts', - key: 'clusterRestarts', - width: 50, + title:
Memory Allocatable (bytes)
, + dataIndex: 'memoryAllocatable', + key: 'memoryAllocatable', + width: 80, sorter: true, align: 'left', }, ]; -export const getK8sClustersListColumns = (): ColumnType[] => - columnsConfig as ColumnType[]; +export const getK8sClustersListColumns = ( + groupBy: IBuilderQuery['groupBy'], +): ColumnType[] => { + if (groupBy.length > 0) { + const filteredColumns = [...columnsConfig].filter( + (column) => column.key !== 'clusterName', + ); + filteredColumns.unshift(clusterGroupColumnConfig); + return filteredColumns as ColumnType[]; + } + + return columnsConfig as ColumnType[]; +}; + +const getGroupByEle = ( + cluster: K8sClustersData, + groupBy: IBuilderQuery['groupBy'], +): React.ReactNode => { + const groupByValues: string[] = []; + + groupBy.forEach((group) => { + groupByValues.push(cluster.meta[group.key as keyof typeof cluster.meta]); + }); -const getStrokeColorForProgressBar = (value: number): string => { - if (value >= 90) return Color.BG_SAKURA_500; - if (value >= 60) return Color.BG_AMBER_500; - return Color.BG_FOREST_500; + return ( +
+ {groupByValues.map((value) => ( + + {value === '' ? '' : value} + + ))} +
+ ); }; export const formatDataForTable = ( data: K8sClustersData[], + groupBy: IBuilderQuery['groupBy'], ): K8sClustersRowData[] => data.map((cluster, index) => ({ key: `${cluster.meta.k8s_cluster_name}-${index}`, + clusterUID: cluster.clusterUID, clusterName: cluster.meta.k8s_cluster_name, - availableReplicas: cluster.availablePods, - desiredReplicas: cluster.desiredPods, - clusterRestarts: cluster.restarts, cpuUtilization: cluster.cpuUsage, - cpuRequestUtilization: ( -
- { - const cpuPercent = Number((cluster.cpuRequest * 100).toFixed(1)); - return getStrokeColorForProgressBar(cpuPercent); - })()} - className="progress-bar" - /> -
- ), - cpuLimitUtilization: ( -
- { - const cpuPercent = Number((cluster.cpuLimit * 100).toFixed(1)); - return getStrokeColorForProgressBar(cpuPercent); - })()} - className="progress-bar" - /> -
- ), memoryUtilization: cluster.memoryUsage, - memoryRequestUtilization: ( -
- { - const memoryPercent = Number((cluster.memoryRequest * 100).toFixed(1)); - return getStrokeColorForProgressBar(memoryPercent); - })()} - className="progress-bar" - /> -
- ), - memoryLimitUtilization: ( -
- { - const memoryPercent = Number((cluster.memoryLimit * 100).toFixed(1)); - return getStrokeColorForProgressBar(memoryPercent); - })()} - className="progress-bar" - /> -
- ), + cpuAllocatable: cluster.cpuAllocatable, + memoryAllocatable: cluster.memoryAllocatable, + clusterGroup: getGroupByEle(cluster, groupBy), + meta: cluster.meta, + ...cluster.meta, + groupedByMeta: cluster.meta, })); diff --git a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx index 48c53b713d4..869be1eb4c6 100644 --- a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx @@ -23,6 +23,7 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall import { useCallback, useState } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import K8sClustersList from './Clusters/K8sClustersList'; import { ClustersQuickFiltersConfig, ContainersQuickFiltersConfig, @@ -36,7 +37,6 @@ import { StatefulsetsQuickFiltersConfig, VolumesQuickFiltersConfig, } from './constants'; -import K8sNamespacesList from './Namespaces/K8sNamespacesList'; import K8sPodLists from './Pods/K8sPodLists'; import Volumes from './Volumes/Volumes'; @@ -359,8 +359,8 @@ export default function InfraMonitoringK8s(): JSX.Element { /> )} - {selectedCategory === K8sCategories.NAMESPACES && ( - diff --git a/frontend/src/container/InfraMonitoringK8s/constants.ts b/frontend/src/container/InfraMonitoringK8s/constants.ts index 311446102ed..7103d94db69 100644 --- a/frontend/src/container/InfraMonitoringK8s/constants.ts +++ b/frontend/src/container/InfraMonitoringK8s/constants.ts @@ -170,7 +170,7 @@ export const NodesQuickFiltersConfig: IQuickFiltersConfig[] = [ export const NamespaceQuickFiltersConfig: IQuickFiltersConfig[] = [ { type: FiltersType.CHECKBOX, - title: 'Namespace Name', + title: 'Namespace', attributeKey: { key: 'k8s_namespace_name', dataType: DataTypes.String, @@ -178,11 +178,11 @@ export const NamespaceQuickFiltersConfig: IQuickFiltersConfig[] = [ isColumn: false, isJSON: false, }, - aggregateOperator: 'noop', - aggregateAttribute: 'k8s_pod_cpu_utilization', - dataSource: DataSource.METRICS, defaultOpen: true, }, +]; + +export const ClustersQuickFiltersConfig: IQuickFiltersConfig[] = [ { type: FiltersType.CHECKBOX, title: 'Cluster Name', @@ -200,21 +200,6 @@ export const NamespaceQuickFiltersConfig: IQuickFiltersConfig[] = [ }, ]; -export const ClustersQuickFiltersConfig: IQuickFiltersConfig[] = [ - { - type: FiltersType.CHECKBOX, - title: 'Clusters', - attributeKey: { - key: 'k8s.cluster.name', - dataType: DataTypes.String, - type: 'resource', - isColumn: false, - isJSON: false, - }, - defaultOpen: true, - }, -]; - export const ContainersQuickFiltersConfig: IQuickFiltersConfig[] = [ { type: FiltersType.CHECKBOX,