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

kharekartik pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 022d1c6856f Add Filter for segment state in the Table UI (#16085)
022d1c6856f is described below

commit 022d1c6856f842c1e341d4505b8450a134af1899
Author: Kartik Khare <[email protected]>
AuthorDate: Wed Jun 18 11:24:45 2025 +0530

    Add Filter for segment state in the Table UI (#16085)
    
    * feat(ui): add segment status filter
    
    * Style segment filter
    
    * Add border above optional table controls
    
    * feat(ui): add status filter component
    
    * Fix chips
    
    * revert chip style changes
    
    * fix license
    
    ---------
    
    Co-authored-by: KKCorps <[email protected]>
---
 .../resources/app/components/SimpleAccordion.tsx   |  68 ++++--
 .../main/resources/app/components/StatusFilter.tsx | 230 +++++++++++++++++++++
 .../src/main/resources/app/components/Table.tsx    |  44 +++-
 .../main/resources/app/components/TableToolbar.tsx |  48 +++--
 .../src/main/resources/app/pages/TenantDetails.tsx |  43 +++-
 5 files changed, 397 insertions(+), 36 deletions(-)

diff --git 
a/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx 
b/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx
index a8e6b3f22f6..a7f9f0b944d 100644
--- a/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx
+++ b/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx
@@ -25,7 +25,7 @@ import AccordionDetails from 
'@material-ui/core/AccordionDetails';
 import Typography from '@material-ui/core/Typography';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 import SearchBar from './SearchBar';
-import { FormControlLabel, Switch, Tooltip } from '@material-ui/core';
+import { FormControlLabel, Switch, Tooltip, Box } from '@material-ui/core';
 import clsx from 'clsx';
 
 const useStyles = makeStyles((theme: Theme) =>
@@ -34,7 +34,7 @@ const useStyles = makeStyles((theme: Theme) =>
       backgroundColor: 'rgba(66, 133, 244, 0.1)',
       borderBottom: '1px #BDCCD9 solid',
       minHeight: '0 !important',
-      '& .MuiAccordionSummary-content.Mui-expanded':{
+      '& .MuiAccordionSummary-content.Mui-expanded': {
         margin: 0,
         alignItems: 'center',
       }
@@ -54,6 +54,26 @@ const useStyles = makeStyles((theme: Theme) =>
       marginRight: 0,
       marginLeft: 'auto',
       zoom: 0.85
+    },
+    controlsContainer: {
+      display: 'flex',
+      alignItems: 'center',
+      gap: theme.spacing(1),
+      padding: '8px 16px',
+      borderBottom: '1px solid #BDCCD9',
+      backgroundColor: '#f8f9fa',
+      flexWrap: 'wrap',
+    },
+    searchBarContainer: {
+      display: 'flex',
+      alignItems: 'center',
+      gap: theme.spacing(1),
+    },
+    additionalControlsContainer: {
+      marginLeft: 'auto',
+      display: 'flex',
+      alignItems: 'center',
+      gap: theme.spacing(1),
     }
   }),
 );
@@ -71,7 +91,8 @@ type Props = {
     toggleName: string;
     toggleValue: boolean;
   },
-  detailsContainerClass?: string
+  detailsContainerClass?: string,
+  additionalControls?: React.ReactNode
 };
 
 export default function SimpleAccordion({
@@ -83,10 +104,14 @@ export default function SimpleAccordion({
   recordCount,
   children,
   accordionToggleObject,
-  detailsContainerClass
+  detailsContainerClass,
+  additionalControls
 }: Props) {
   const classes = useStyles();
 
+  const hasControls = showSearchBox || additionalControls;
+  const needsControlsContainer = additionalControls; // Only create container 
when there are additional controls
+
   return (
     <Accordion
       defaultExpanded={true}
@@ -110,7 +135,7 @@ export default function SimpleAccordion({
             control={
               <Switch
                 checked={accordionToggleObject.toggleValue}
-                onChange={accordionToggleObject.toggleChangeHandler} 
+                onChange={accordionToggleObject.toggleChangeHandler}
                 name={accordionToggleObject.toggleName}
                 color="primary"
               />
@@ -120,13 +145,32 @@ export default function SimpleAccordion({
         }
       </AccordionSummary>
       <AccordionDetails className={clsx(classes.details, 
detailsContainerClass)}>
-        {showSearchBox ?
-          <SearchBar
-            // searchOnRight={true}
-            value={searchValue}
-            onChange={(e) => handleSearch(e.target.value)}
-          />
-          : null}
+        {needsControlsContainer ? (
+          // New layout: search + additional controls in container
+          <div className={classes.controlsContainer}>
+            {showSearchBox && (
+              <div className={classes.searchBarContainer}>
+                <SearchBar
+                  value={searchValue}
+                  onChange={(e) => handleSearch(e.target.value)}
+                />
+              </div>
+            )}
+            {additionalControls && (
+              <div className={classes.additionalControlsContainer}>
+                {additionalControls}
+              </div>
+            )}
+          </div>
+        ) : (
+          // Original layout: just search bar if present
+          showSearchBox && (
+            <SearchBar
+              value={searchValue}
+              onChange={(e) => handleSearch(e.target.value)}
+            />
+          )
+        )}
         {children}
       </AccordionDetails>
     </Accordion>
diff --git 
a/pinot-controller/src/main/resources/app/components/StatusFilter.tsx 
b/pinot-controller/src/main/resources/app/components/StatusFilter.tsx
new file mode 100644
index 00000000000..60f97cd9faa
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/StatusFilter.tsx
@@ -0,0 +1,230 @@
+/**
+ * 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 {
+  FormControl,
+  InputLabel,
+  Select,
+  MenuItem,
+  makeStyles,
+  Chip
+} from '@material-ui/core';
+import { DISPLAY_SEGMENT_STATUS } from 'Models';
+
+const useStyles = makeStyles((theme) => ({
+  formControl: {
+    minWidth: 140,
+    height: 32, // Match search bar height
+  },
+  select: {
+    height: 32,
+    fontSize: '0.875rem',
+    backgroundColor: '#fff',
+    '& .MuiSelect-select': {
+      paddingTop: 6,
+      paddingBottom: 6,
+      paddingLeft: 12,
+      paddingRight: 32,
+      display: 'flex',
+      alignItems: 'center',
+      height: 'auto',
+      minHeight: 'unset',
+    },
+    '& .MuiOutlinedInput-root': {
+      borderRadius: 4,
+      '&:hover .MuiOutlinedInput-notchedOutline': {
+        borderColor: '#4285f4',
+      },
+      '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
+        borderColor: '#4285f4',
+        borderWidth: 1,
+      },
+    },
+    '& .MuiOutlinedInput-notchedOutline': {
+      borderColor: '#BDCCD9',
+    },
+  },
+  inputLabel: {
+    fontSize: '0.75rem',
+    color: '#666',
+    transform: 'translate(12px, 9px) scale(1)',
+    '&.MuiInputLabel-shrink': {
+      transform: 'translate(12px, -6px) scale(0.75)',
+      color: '#4285f4',
+    },
+    '&.Mui-focused': {
+      color: '#4285f4',
+    },
+  },
+  menuItem: {
+    padding: '6px 12px',
+    fontSize: '0.875rem',
+    minHeight: 'auto',
+    '&:hover': {
+      backgroundColor: 'rgba(66, 133, 244, 0.08)',
+    },
+    '&.Mui-selected': {
+      backgroundColor: 'rgba(66, 133, 244, 0.12)',
+      '&:hover': {
+        backgroundColor: 'rgba(66, 133, 244, 0.16)',
+      },
+    },
+  },
+  statusChip: {
+    height: 18,
+    fontSize: '0.7rem',
+    fontWeight: 600,
+    marginLeft: 6,
+    '& .MuiChip-label': {
+      paddingLeft: 6,
+      paddingRight: 6,
+    }
+  },
+  // Status styles
+  cellStatusGood: {
+    color: '#4CAF50',
+    backgroundColor: 'rgba(76, 175, 80, 0.1)',
+    border: '1px solid #4CAF50',
+  },
+  cellStatusBad: {
+    color: '#f44336',
+    backgroundColor: 'rgba(244, 67, 54, 0.1)',
+    border: '1px solid #f44336',
+  },
+  cellStatusConsuming: {
+    color: '#ff9800',
+    backgroundColor: 'rgba(255, 152, 0, 0.1)',
+    border: '1px solid #ff9800',
+  },
+  cellStatusError: {
+    color: '#a11',
+    backgroundColor: 'rgba(170, 17, 17, 0.1)',
+    border: '1px solid #a11',
+  },
+  menuPaper: {
+    marginTop: 2,
+    boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.15)',
+    border: '1px solid #BDCCD9',
+    maxHeight: 200,
+  }
+}));
+
+type StatusFilterOption = {
+  label: string;
+  value: 'ALL' | DISPLAY_SEGMENT_STATUS | 'BAD_OR_UPDATING';
+};
+
+type StatusFilterProps = {
+  value: 'ALL' | DISPLAY_SEGMENT_STATUS | 'BAD_OR_UPDATING';
+  onChange: (value: 'ALL' | DISPLAY_SEGMENT_STATUS | 'BAD_OR_UPDATING') => 
void;
+  options: StatusFilterOption[];
+};
+
+export const getStatusChipClass = (status: string, classes?: any) => {
+  const normalizedStatus = status.toLowerCase();
+  switch (normalizedStatus) {
+    case DISPLAY_SEGMENT_STATUS.GOOD.toLowerCase():
+      return classes.cellStatusGood;
+    case DISPLAY_SEGMENT_STATUS.BAD.toLowerCase():
+      return classes.cellStatusBad;
+    case DISPLAY_SEGMENT_STATUS.UPDATING.toLowerCase():
+      return classes.cellStatusConsuming;
+    case 'error':
+      return classes.cellStatusError;
+    case 'bad_or_updating':
+      return classes.cellStatusBad;
+    default:
+      return '';
+  }
+};
+
+const StatusFilter: React.FC<StatusFilterProps> = ({ value, onChange, options 
}) => {
+  const classes = useStyles();
+
+  const renderValue = (selected: string) => {
+    const selectedOption = options.find(option => option.value === selected);
+    const label = selectedOption ? selectedOption.label : 'All';
+
+    if (selected === 'ALL') {
+      return label;
+    }
+
+    return (
+      <div style={{ display: 'flex', alignItems: 'center' }}>
+        <Chip
+          size="small"
+          label={label}
+          variant="outlined"
+          className={`${classes.statusChip} ${getStatusChipClass(selected, 
classes)}`}
+        />
+      </div>
+    );
+  };
+
+  return (
+    <FormControl variant="outlined" className={classes.formControl} 
size="small">
+      <InputLabel className={classes.inputLabel}>Filter</InputLabel>
+      <Select
+        value={value}
+        onChange={(e) => onChange(e.target.value as 'ALL' | 
DISPLAY_SEGMENT_STATUS | 'BAD_OR_UPDATING')}
+        label="Filter"
+        className={classes.select}
+        renderValue={renderValue}
+        MenuProps={{
+          PaperProps: {
+            className: classes.menuPaper,
+          },
+          anchorOrigin: {
+            vertical: 'bottom',
+            horizontal: 'left',
+          },
+          transformOrigin: {
+            vertical: 'top',
+            horizontal: 'left',
+          },
+          getContentAnchorEl: null,
+        }}
+      >
+        {options.map((option) => (
+          <MenuItem
+            key={option.value}
+            value={option.value}
+            className={classes.menuItem}
+          >
+            <div style={{
+              display: 'flex',
+              alignItems: 'center',
+              width: '100%',
+              justifyContent: 'space-between'
+            }}>
+              <Chip
+                size="small"
+                label={option.label}
+                variant="outlined"
+                className={`${classes.statusChip} 
${getStatusChipClass(option.value, classes)}`}
+              />
+            </div>
+          </MenuItem>
+        ))}
+      </Select>
+    </FormControl>
+  );
+};
+
+export default StatusFilter;
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Table.tsx 
b/pinot-controller/src/main/resources/app/components/Table.tsx
index 7b866f38b7a..399fd75072d 100644
--- a/pinot-controller/src/main/resources/app/components/Table.tsx
+++ b/pinot-controller/src/main/resources/app/components/Table.tsx
@@ -51,6 +51,8 @@ import { sortBytes, sortNumberOfSegments } from 
'../utils/SortFunctions'
 import Utils from '../utils/Utils';
 import TableToolbar from './TableToolbar';
 import SimpleAccordion from './SimpleAccordion';
+import clsx from 'clsx';
+import { getStatusChipClass } from './StatusFilter';
 
 type Props = {
   title?: string,
@@ -72,7 +74,8 @@ type Props = {
     toggleName: string;
     toggleValue: boolean;
   },
-  tooltipData?: string[]
+  tooltipData?: string[],
+  additionalControls?: React.ReactNode
 };
 
 // These sort functions are applied to any columns with these names. 
Otherwise, we just
@@ -165,6 +168,14 @@ const useStyles = makeStyles((theme) => ({
   spacer: {
     flex: '0 1 auto',
   },
+  chip: {
+    height: 24,
+    '& span': {
+      paddingLeft: 8,
+      paddingRight: 8,
+      fontWeight: 600,
+    },
+  },
   cellStatusGood: {
     color: '#4CAF50',
     border: '1px solid #4CAF50',
@@ -281,7 +292,8 @@ export default function CustomizedTables({
   inAccordionFormat,
   regexReplace,
   accordionToggleObject,
-  tooltipData
+  tooltipData,
+  additionalControls
 }: Props) {
   // Separate the initial and final data into two separated state variables.
   // This way we can filter and sort the data without affecting the original 
data.
@@ -365,14 +377,14 @@ export default function CustomizedTables({
 
   const styleCell = (str: string) => {
     if (str.toLowerCase() === 'good' || str.toLowerCase() === 'online' || 
str.toLowerCase() === 'alive' || str.toLowerCase() === 'true') {
-      return (
-        <StyledChip
-          label={str}
-          className={classes.cellStatusGood}
-          variant="outlined"
-        />
-      );
-    }
+          return (
+            <StyledChip
+              label={str}
+              className={classes.cellStatusGood}
+              variant="outlined"
+            />
+          );
+        }
     if (str.toLocaleLowerCase() === 'bad' || str.toLowerCase() === 'offline' 
|| str.toLowerCase() === 'dead' || str.toLowerCase() === 'false') {
       return (
         <StyledChip
@@ -604,6 +616,17 @@ export default function CustomizedTables({
           handleSearch={(val: string) => setSearch(val)}
           recordCount={recordsCount}
         />
+        {additionalControls && (
+          <div
+            style={{
+              marginTop: 8,
+              paddingTop: 8,
+              borderTop: '1px solid #BDCCD9',
+            }}
+          >
+            {additionalControls}
+          </div>
+        )}
         {renderTableComponent()}
       </>
     );
@@ -619,6 +642,7 @@ export default function CustomizedTables({
           handleSearch={(val: string) => setSearch(val)}
           recordCount={recordsCount}
           accordionToggleObject={accordionToggleObject}
+          additionalControls={additionalControls}
         >
           {renderTableComponent()}
         </SimpleAccordion>
diff --git 
a/pinot-controller/src/main/resources/app/components/TableToolbar.tsx 
b/pinot-controller/src/main/resources/app/components/TableToolbar.tsx
index 6905bdb3b45..0d2a9ab9c46 100644
--- a/pinot-controller/src/main/resources/app/components/TableToolbar.tsx
+++ b/pinot-controller/src/main/resources/app/components/TableToolbar.tsx
@@ -18,7 +18,7 @@
  */
 
 import * as React from 'react';
-import { Typography, Toolbar, Tooltip } from '@material-ui/core';
+import { Typography, Toolbar, Tooltip, Box } from '@material-ui/core';
 import {
   makeStyles
 } from '@material-ui/core/styles';
@@ -33,6 +33,7 @@ type Props = {
   recordCount?: number;
   showTooltip?: boolean;
   tooltipText?: string;
+  additionalControls?: React.ReactNode;
 };
 
 const useToolbarStyles = makeStyles((theme) => ({
@@ -40,7 +41,9 @@ const useToolbarStyles = makeStyles((theme) => ({
     paddingLeft: '15px',
     paddingRight: '15px',
     minHeight: 48,
-    backgroundColor: 'rgba(66, 133, 244, 0.1)'
+    backgroundColor: 'rgba(66, 133, 244, 0.1)',
+    display: 'flex',
+    alignItems: 'center',
   },
   title: {
     flex: '1 1 auto',
@@ -49,6 +52,16 @@ const useToolbarStyles = makeStyles((theme) => ({
     fontSize: '1rem',
     color: '#4285f4'
   },
+  controlsContainer: {
+    display: 'flex',
+    alignItems: 'center',
+    gap: theme.spacing(1),
+  },
+  recordCount: {
+    fontWeight: 600,
+    color: '#666',
+    fontSize: '0.875rem',
+  }
 }));
 
 export default function TableToolbar({
@@ -58,7 +71,8 @@ export default function TableToolbar({
   handleSearch,
   recordCount,
   showTooltip,
-  tooltipText
+  tooltipText,
+  additionalControls
 }: Props) {
   const classes = useToolbarStyles();
 
@@ -72,15 +86,25 @@ export default function TableToolbar({
       >
         {name.toUpperCase()}
       </Typography>
-      {showSearchBox ? <SearchBar
-        value={searchValue}
-        onChange={(e) => handleSearch(e.target.value)}
-      /> : <strong>{(recordCount)}</strong>}
-      {showTooltip &&
-        <Tooltip title={tooltipText}>
-          <HelpOutlineIcon />
-        </Tooltip>
-      }
+
+      <div className={classes.controlsContainer}>
+        {additionalControls}
+
+        {showSearchBox ? (
+          <SearchBar
+            value={searchValue}
+            onChange={(e) => handleSearch(e.target.value)}
+          />
+        ) : (
+          <span className={classes.recordCount}>{recordCount}</span>
+        )}
+
+        {showTooltip && (
+          <Tooltip title={tooltipText}>
+            <HelpOutlineIcon />
+          </Tooltip>
+        )}
+      </div>
     </Toolbar>
   );
 }
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx 
b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
index 893f22e65f9..37862ec5acc 100644
--- a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
+++ b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, useMemo } from 'react';
 import { makeStyles } from '@material-ui/core/styles';
 import { Box, Button, Checkbox, FormControlLabel, Grid, Switch, Tooltip, 
Typography, CircularProgress, Menu, MenuItem, Chip } from '@material-ui/core';
 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
@@ -50,6 +50,7 @@ import {
   RebalanceServerStatusOp
 } from "../components/Homepage/Operations/RebalanceServerStatusOp";
 import ConsumingSegmentsTable from '../components/ConsumingSegmentsTable';
+import StatusFilter from '../components/StatusFilter';
 
 const useStyles = makeStyles((theme) => ({
   root: {
@@ -157,6 +158,30 @@ const TenantPageDetails = ({ match }: 
RouteComponentProps<Props>) => {
   const segmentListColumns = ['Segment Name', 'Status'];
   const loadingSegmentList = Utils.getLoadingTableData(segmentListColumns);
   const [segmentList, setSegmentList] = 
useState<TableData>(loadingSegmentList);
+  const [segmentStatusFilter, setSegmentStatusFilter] = useState<'ALL' | 
DISPLAY_SEGMENT_STATUS | 'BAD_OR_UPDATING'>('ALL');
+  const displaySegmentList = useMemo(() => {
+    const filtered = segmentList.records.filter(([_, status]) => {
+      const value = typeof status === 'object' && status !== null && 'value' 
in status ? status.value as DISPLAY_SEGMENT_STATUS : status as 
DISPLAY_SEGMENT_STATUS;
+      if (segmentStatusFilter === 'ALL') return true;
+      if (segmentStatusFilter === 'BAD_OR_UPDATING') return value !== 
DISPLAY_SEGMENT_STATUS.GOOD;
+      return value === segmentStatusFilter;
+    });
+    return { ...segmentList, records: filtered };
+  }, [segmentList, segmentStatusFilter]);
+
+  const segmentStatusFilterElement = (
+    <StatusFilter
+      value={segmentStatusFilter}
+      onChange={setSegmentStatusFilter}
+      options={[
+        { label: 'All', value: 'ALL' },
+        { label: 'Bad or Updating', value: 'BAD_OR_UPDATING' },
+        { label: 'Bad', value: DISPLAY_SEGMENT_STATUS.BAD },
+        { label: 'Updating', value: DISPLAY_SEGMENT_STATUS.UPDATING },
+        { label: 'Good', value: DISPLAY_SEGMENT_STATUS.GOOD },
+      ]}
+    />
+  );
 
   const [tableSchema, setTableSchema] = useState<TableData>({
     columns: [],
@@ -248,6 +273,7 @@ const TenantPageDetails = ({ match }: 
RouteComponentProps<Props>) => {
       segmentTableRows.push([
         name,
         {
+          value: status,
           customRenderer: (
             <SegmentStatusRenderer
               segmentName={name}
@@ -869,7 +895,7 @@ const TenantPageDetails = ({ match }: 
RouteComponentProps<Props>) => {
               </div>
             <CustomizedTables
               title={"Segments - " + segmentList.records.length}
-              data={segmentList}
+              data={displaySegmentList}
               baseURL={
                 tenantName && `/tenants/${tenantName}/table/${tableName}/` ||
                 instanceName && 
`/instance/${instanceName}/table/${tableName}/` ||
@@ -878,6 +904,19 @@ const TenantPageDetails = ({ match }: 
RouteComponentProps<Props>) => {
               addLinks
               showSearchBox={true}
               inAccordionFormat={true}
+              additionalControls={
+                <StatusFilter
+                  value={segmentStatusFilter}
+                  onChange={setSegmentStatusFilter}
+                  options={[
+                    { label: 'All', value: 'ALL' },
+                    { label: 'Bad or Updating', value: 'BAD_OR_UPDATING' },
+                    { label: 'Bad', value: DISPLAY_SEGMENT_STATUS.BAD },
+                    { label: 'Updating', value: 
DISPLAY_SEGMENT_STATUS.UPDATING },
+                    { label: 'Good', value: DISPLAY_SEGMENT_STATUS.GOOD },
+                  ]}
+                />
+              }
             />
           </Grid>
           <Grid item xs={6}>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to