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]