diff --git a/client/src/components/grid/Grid.tsx b/client/src/components/grid/Grid.tsx index 93cae0fe..6082ea87 100644 --- a/client/src/components/grid/Grid.tsx +++ b/client/src/components/grid/Grid.tsx @@ -32,6 +32,7 @@ import type { AttuGridType } from './Types'; * @param handlesort how to sort table, if it's undefined, then you can not sort table * @param order 'desc' | 'asc'. sort direction * @param order order by which table field + * @param addSpacerColumn control spacer column display. default is false * @returns */ const AttuGrid: FC = props => { @@ -44,8 +45,8 @@ const AttuGrid: FC = props => { const { rowCount = 20, rowsPerPage = 10, - tableHeaderHeight = 43.5, - rowHeight = 41, + tableHeaderHeight = 44, + rowHeight = 42, pagerHeight = 52, primaryKey = 'id', showToolbar = false, @@ -79,6 +80,7 @@ const AttuGrid: FC = props => { hideOnDisable, rowDecorator = () => ({}), sx = {}, + addSpacerColumn = false, } = props; const _isSelected = (row: { [x: string]: any }) => { @@ -131,7 +133,7 @@ const AttuGrid: FC = props => { containerHeight - tableHeaderHeight - (showPagination ? pagerHeight : 0) - - (hasToolbar ? 47 : 0); + (hasToolbar ? 42 : 0); const rowCount = Math.floor(totalHeight / rowHeight); @@ -226,6 +228,7 @@ const AttuGrid: FC = props => { orderBy={orderBy} rowHeight={rowHeight} rowDecorator={rowDecorator} + addSpacerColumn={addSpacerColumn} > {rowCount && showPagination ? ( + * + * The spacer column will: + * - Have width: 'auto' and minWidth: 'auto' + * - Display no content + * - Naturally absorb remaining horizontal space without forcing table width + * - Prevent unnecessary horizontal scrollbars + */ + const EnhancedTable: FC = props => { let { selected, @@ -44,10 +64,30 @@ const EnhancedTable: FC = props => { orderBy, rowDecorator = () => ({}), rowHeight, + // whether to add a spacer column to control space distribution + addSpacerColumn = false, } = props; const hasData = rows && rows.length > 0; + // Add spacer column definition if needed + const finalColDefinitions = addSpacerColumn + ? [ + ...colDefinitions, + { + id: '__spacer__', + align: 'left' as const, + disablePadding: false, + label: '', + getStyle: () => ({ + width: '66.7%', + minWidth: 'auto', + }), + formatter: () => null, + }, + ] + : colDefinitions; + return ( ({ @@ -70,7 +110,7 @@ const EnhancedTable: FC = props => { > {!headEditable ? ( = props => { )} - {colDefinitions.map((colDef, i) => { + {finalColDefinitions.map((colDef, i) => { const { actionBarConfigs = [], needCopy = false } = colDef; const cellStyleFromDef = colDef.getStyle ? colDef.getStyle(row[colDef.id]) : {}; + + // Skip rendering for spacer column + if (colDef.id === '__spacer__') { + return ( + ({ + borderBottom: `1px solid ${theme.palette.divider}`, + }), + cellStyleFromDef, + ]} + > + {/* Empty content for spacer column */} + + ); + } + return colDef.showActionCell ? ( = props => { })} colSpan={ openCheckBox - ? colDefinitions.length + 1 - : colDefinitions.length + ? finalColDefinitions.length + 1 + : finalColDefinitions.length } > {noData} @@ -344,8 +402,8 @@ const EnhancedTable: FC = props => { })} colSpan={ openCheckBox - ? colDefinitions.length + 1 - : colDefinitions.length + ? finalColDefinitions.length + 1 + : finalColDefinitions.length } > diff --git a/client/src/components/grid/Types.ts b/client/src/components/grid/Types.ts index f52966a5..7ea70d7c 100644 --- a/client/src/components/grid/Types.ts +++ b/client/src/components/grid/Types.ts @@ -99,6 +99,8 @@ export type TableType = { order?: SortDirection; orderBy?: string; ref?: Ref; + // whether to add a spacer column to control space distribution + addSpacerColumn?: boolean; }; export type ColDefinitionsType = { @@ -159,6 +161,8 @@ export type AttuGridType = ToolBarType & { pagerHeight?: number; rowDecorator?: (row: any) => SxProps | React.CSSProperties; sx?: SxProps; + // whether to add a spacer column to control space distribution + addSpacerColumn?: boolean; }; export type ActionBarType = { diff --git a/client/src/hooks/Query.ts b/client/src/hooks/Query.ts index c525b4c7..0ad022d8 100644 --- a/client/src/hooks/Query.ts +++ b/client/src/hooks/Query.ts @@ -59,23 +59,24 @@ export const useQuery = (params: { }; // query function - const query = async ( - page: number = currentPage, - consistency_level = queryState.consistencyLevel, - _outputFields = queryState.outputFields, - expr = queryState.expr - ) => { + const query = async (page: number = currentPage, queryState: QueryState) => { if (!collection || !collection.loaded) return; - const _expr = getPageExpr(page, expr); + const _expr = getPageExpr(page, queryState.expr); onQueryStart(_expr); + // each queryState.outputFields can not be a function output + const outputFields = queryState.outputFields.filter( + f => + !collection.schema.fields.find(ff => ff.name === f)?.is_function_output + ); + try { const queryParams = { expr: _expr, - output_fields: _outputFields, + output_fields: outputFields, limit: pageSize || 10, - consistency_level, + consistency_level: queryState.consistencyLevel, // travel_timestamp: timeTravelInfo.timestamp, }; @@ -111,7 +112,7 @@ export const useQuery = (params: { onQueryEnd?.(res); } catch (e: any) { - reset(); + reset(true); } finally { onQueryFinally?.(); } @@ -142,9 +143,11 @@ export const useQuery = (params: { }; // reset - const reset = () => { + const reset = (clearData = false) => { setCurrentPage(0); - setQueryResult({ data: [], latency: 0 }); + if (clearData) { + setQueryResult({ data: [], latency: 0 }); + } pageCache.current.clear(); }; @@ -166,12 +169,7 @@ export const useQuery = (params: { count(queryState.consistencyLevel, queryState.expr); // Then fetch actual data - query( - currentPage, - queryState.consistencyLevel, - queryState.outputFields, - queryState.expr - ); + query(currentPage, queryState); }, [ pageSize, queryState.outputFields, @@ -181,6 +179,19 @@ export const useQuery = (params: { queryState.tick, ]); + // Reset state when collection changes + useEffect(() => { + // Immediately reset when collection changes to avoid showing stale data + setQueryResult({ data: [], latency: 0 }); + setCurrentPage(0); + pageCache.current.clear(); + + // Set total to collection row count as fallback + if (collection) { + setTotal(collection.rowCount || 0); + } + }, [collection.collection_name]); + return { // total query count total, diff --git a/client/src/pages/databases/Databases.tsx b/client/src/pages/databases/Databases.tsx index 5dee8f9b..1a7b6242 100644 --- a/client/src/pages/databases/Databases.tsx +++ b/client/src/pages/databases/Databases.tsx @@ -227,9 +227,7 @@ const Databases = () => { // if query state not found, and the schema is ready, create new query state if (!query && c.schema) { setQueryState(prevState => { - const fields = [...c.schema.fields].filter( - f => !f.is_function_output - ); + const fields = c.schema.fields; return [ ...prevState, { diff --git a/client/src/pages/databases/collections/Collections.tsx b/client/src/pages/databases/collections/Collections.tsx index 518b9312..570d667b 100644 --- a/client/src/pages/databases/collections/Collections.tsx +++ b/client/src/pages/databases/collections/Collections.tsx @@ -468,8 +468,8 @@ const Collections = () => { page={currentPage} onPageChange={handlePageChange} rowsPerPage={pageSize} - tableHeaderHeight={46} - rowHeight={41} + tableHeaderHeight={44} + rowHeight={42} setRowsPerPage={handlePageSize} isLoading={loading} handleSort={handleGridSort} diff --git a/client/src/pages/databases/collections/data/CollectionData.tsx b/client/src/pages/databases/collections/data/CollectionData.tsx index c3cb59c9..58f49b26 100644 --- a/client/src/pages/databases/collections/data/CollectionData.tsx +++ b/client/src/pages/databases/collections/data/CollectionData.tsx @@ -33,11 +33,13 @@ const CollectionData = (props: CollectionDataProps) => { } // UI state - const [tableLoading, setTableLoading] = useState(); + const [tableLoading, setTableLoading] = useState(false); const [selectedData, setSelectedData] = useState([]); const exprInputRef = useRef(queryState.expr); const [, forceUpdate] = useState({}); const loadingTimeoutRef = useRef(null); + const [isCollectionSwitching, setIsCollectionSwitching] = + useState(false); // UI functions const { fetchCollection } = useContext(dataContext); @@ -68,10 +70,10 @@ const CollectionData = (props: CollectionDataProps) => { clearTimeout(loadingTimeoutRef.current); } - // Set a timeout to show loading after 100ms + // Set a timeout to show loading after 200ms to avoid flickering for fast queries loadingTimeoutRef.current = setTimeout(() => { setTableLoading(true); - }, 100); + }, 200); if (expr === '') { handleFilterReset(); @@ -85,6 +87,7 @@ const CollectionData = (props: CollectionDataProps) => { loadingTimeoutRef.current = null; } setTableLoading(false); + setIsCollectionSwitching(false); }, queryState: queryState, setQueryState: setQueryState, @@ -143,15 +146,12 @@ const CollectionData = (props: CollectionDataProps) => { tick: queryState.tick + 1, }); forceUpdate({}); - - // ensure not loading - setTableLoading(false); }, [collection.schema.fields, queryState, setQueryState]); const handlePageChange = useCallback( async (e: any, page: number) => { // do the query - await query(page, queryState.consistencyLevel); + await query(page, queryState); // update page number setCurrentPage(page); }, @@ -170,7 +170,7 @@ const CollectionData = (props: CollectionDataProps) => { // update count count(ConsistencyLevelEnum.Strong); // update query - query(0, ConsistencyLevelEnum.Strong); + query(0, { ...queryState, consistencyLevel: ConsistencyLevelEnum.Strong }); }, [count, query, reset]); const onInsert = useCallback( @@ -227,6 +227,9 @@ const CollectionData = (props: CollectionDataProps) => { exprInputRef.current = queryState.expr; forceUpdate({}); + // Set collection switching state when collection changes + setIsCollectionSwitching(true); + // Clean up timeout on unmount or when collection changes return () => { if (loadingTimeoutRef.current) { @@ -261,20 +264,50 @@ const CollectionData = (props: CollectionDataProps) => { handleFilterSubmit={handleFilterSubmit} handleFilterReset={handleFilterReset} setCurrentPage={setCurrentPage} + forceDisabled={isCollectionSwitching} /> { + const field = collection.schema.fields.find(f => f.name === i); + + // if field is function output, return a special col definition + if (field?.is_function_output) { + const inputFields = collection.schema.functions.find(fn => + fn.output_field_names?.includes(i) + )?.input_field_names; + + return { + id: collection.schema.primaryField.name, + align: 'left', + disablePadding: false, + needCopy: false, + label: i, + formatter: () => ( + theme.palette.text.disabled }} + > + auto-generated from {inputFields?.join(', ')} + + ), + headerFormatter: v => { + return ( + + ); + }, + }; + } + return { id: i, align: 'left', disablePadding: false, needCopy: true, formatter(_: any, cellData: any) { - const field = collection.schema.fields.find( - f => f.name === i - ); - const fieldType = field?.data_type || 'JSON'; // dynamic return ; @@ -289,18 +322,20 @@ const CollectionData = (props: CollectionDataProps) => { })} primaryKey={collection.schema.primaryField.name} openCheckBox={true} - isLoading={tableLoading} + isLoading={ + tableLoading || (isCollectionSwitching && collection.loaded) + } rows={queryResult.data} rowCount={total} selected={selectedData} - tableHeaderHeight={43.5} - rowHeight={43} setSelected={onSelectChange} page={currentPage} onPageChange={handlePageChange} setRowsPerPage={setPageSize} rowsPerPage={pageSize} - showPagination={!tableLoading && collection.loaded} + showPagination={ + !tableLoading && !isCollectionSwitching && collection.loaded + } labelDisplayedRows={getLabelDisplayedRows( commonTrans( queryResult.data.length > 1 ? 'grid.entities' : 'grid.entity' @@ -343,7 +378,7 @@ const CollectionData = (props: CollectionDataProps) => { )} noData={searchTrans( - `${collection.loaded ? 'empty' : 'collectionNotLoaded'}` + !collection.loaded ? 'collectionNotLoaded' : 'empty' )} /> diff --git a/client/src/pages/databases/collections/data/QueryToolbar.tsx b/client/src/pages/databases/collections/data/QueryToolbar.tsx index 2ade1d62..ab646821 100644 --- a/client/src/pages/databases/collections/data/QueryToolbar.tsx +++ b/client/src/pages/databases/collections/data/QueryToolbar.tsx @@ -19,6 +19,7 @@ interface QueryToolbarProps { handleFilterSubmit: (expression: string) => Promise; handleFilterReset: () => Promise; setCurrentPage: (page: number) => void; + forceDisabled?: boolean; } const QueryToolbar = (props: QueryToolbarProps) => { @@ -32,6 +33,7 @@ const QueryToolbar = (props: QueryToolbarProps) => { handleFilterSubmit, handleFilterReset, setCurrentPage, + forceDisabled = false, } = props; // translations @@ -50,7 +52,7 @@ const QueryToolbar = (props: QueryToolbarProps) => { value={exprInputRef.current} onChange={handleExprChange} onKeyDown={handleExprKeyDown} - disabled={!collection.loaded} + disabled={!collection.loaded || forceDisabled} fields={collection.schema.scalarFields} onSubmit={handleFilterSubmit} /> @@ -58,7 +60,7 @@ const QueryToolbar = (props: QueryToolbarProps) => { {collectionTrans('consistency')} @@ -121,7 +123,7 @@ const QueryToolbar = (props: QueryToolbarProps) => { )}`} ); }} - disabled={!collection.loaded} + disabled={!collection.loaded || forceDisabled} sx={{ width: '120px', marginTop: '1px', @@ -148,7 +150,7 @@ const QueryToolbar = (props: QueryToolbarProps) => { } > {btnTrans('reset')} @@ -165,7 +167,7 @@ const QueryToolbar = (props: QueryToolbarProps) => { tick: queryState.tick + 1, }); }} - disabled={!collection.loaded} + disabled={!collection.loaded || forceDisabled} > {btnTrans('query')} diff --git a/client/src/pages/databases/collections/partitions/Partitions.tsx b/client/src/pages/databases/collections/partitions/Partitions.tsx index 2e11b1f8..6cf00b5a 100644 --- a/client/src/pages/databases/collections/partitions/Partitions.tsx +++ b/client/src/pages/databases/collections/partitions/Partitions.tsx @@ -181,11 +181,6 @@ const Partitions = () => { needCopy: true, disablePadding: false, label: t('id'), - getStyle: () => { - return { - width: 120, - }; - }, }, { id: 'name', @@ -256,13 +251,23 @@ const Partitions = () => { }; return ( - + + {' '} { disablePadding: false, label: t('property'), needCopy: true, - getStyle: () => { - return { - minWidth: 150, - }; - }, }, { id: 'value', @@ -168,11 +163,6 @@ const Properties = (props: PropertiesProps) => { return obj.type === 'number' ? formatNumber(obj.value) : obj.value; } }, - getStyle: () => { - return { - minWidth: 450, - }; - }, }, ]; diff --git a/client/src/pages/databases/collections/schema/Schema.tsx b/client/src/pages/databases/collections/schema/Schema.tsx index 816b3f0d..e0a96764 100644 --- a/client/src/pages/databases/collections/schema/Schema.tsx +++ b/client/src/pages/databases/collections/schema/Schema.tsx @@ -622,6 +622,8 @@ const Overview = () => { primaryKey="fieldID" showHoverStyle={false} isLoading={loading} + rowHeight={44} + tableHeaderHeight={44} openCheckBox={false} showPagination={false} labelDisplayedRows={getLabelDisplayedRows( diff --git a/client/src/pages/databases/collections/search/Search.tsx b/client/src/pages/databases/collections/search/Search.tsx index af90f00a..850598c9 100644 --- a/client/src/pages/databases/collections/search/Search.tsx +++ b/client/src/pages/databases/collections/search/Search.tsx @@ -341,17 +341,6 @@ const Search = (props: CollectionDataProps) => { /> ); }, - getStyle: d => { - const field = collection.schema.fields.find( - f => f.name === key - ); - if (!d || !field) { - return {}; - } - return { - minWidth: getColumnWidth(field), - }; - }, }; }) : []; @@ -645,8 +634,8 @@ const Search = (props: CollectionDataProps) => { rowCount={total} primaryKey="rank" page={currentPage} - tableHeaderHeight={46} - rowHeight={41} + tableHeaderHeight={45} + rowHeight={42} openCheckBox={false} onPageChange={handlePageChange} rowsPerPage={pageSize} diff --git a/client/src/pages/databases/collections/segments/Segments.tsx b/client/src/pages/databases/collections/segments/Segments.tsx index f44f63e1..87a9061b 100644 --- a/client/src/pages/databases/collections/segments/Segments.tsx +++ b/client/src/pages/databases/collections/segments/Segments.tsx @@ -109,22 +109,12 @@ const Segments = () => { disablePadding: false, needCopy: true, label: 'ID', - getStyle: () => { - return { - minWidth: 155, - }; - }, }, { id: 'level', align: 'left', disablePadding: false, label: 'Level', - getStyle: () => { - return { - minWidth: 15, - }; - }, }, { id: 'partitionID', @@ -132,33 +122,18 @@ const Segments = () => { disablePadding: false, needCopy: true, label: collectionTrans('partitionID'), - getStyle: () => { - return { - minWidth: 160, - }; - }, }, { id: 'state', align: 'left', disablePadding: false, label: collectionTrans('segPState'), - getStyle: () => { - return { - minWidth: 160, - }; - }, }, { id: 'num_rows', align: 'left', disablePadding: false, label: collectionTrans('num_rows'), - getStyle: () => { - return { - minWidth: 70, - }; - }, }, { id: 'q_nodeIds', @@ -168,22 +143,12 @@ const Segments = () => { formatter(data, cellData, cellIndex) { return cellData.join(','); }, - getStyle: () => { - return { - minWidth: 35, - }; - }, }, { id: 'q_state', align: 'left', disablePadding: false, label: collectionTrans('q_state'), - getStyle: () => { - return { - minWidth: 70, - }; - }, }, // { // id: 'q_index_name', @@ -228,6 +193,8 @@ const Segments = () => { toolbarConfigs={[]} colDefinitions={colDefinitions} rows={data} + rowHeight={43} + tableHeaderHeight={45} rowCount={total} primaryKey="name" showPagination={true} diff --git a/client/src/pages/user/User.tsx b/client/src/pages/user/User.tsx index d44ea668..9c6c179e 100644 --- a/client/src/pages/user/User.tsx +++ b/client/src/pages/user/User.tsx @@ -231,9 +231,6 @@ const Users = () => { formatter(rowData, cellData) { return rowData.username === 'root' ? 'admin' : cellData.join(', '); }, - getStyle: () => { - return { width: '80%', maxWidth: '80%' }; - }, }, ]; @@ -259,8 +256,8 @@ const Users = () => { primaryKey="username" showPagination={true} selected={selectedUser} - tableHeaderHeight={46} - rowHeight={39} + tableHeaderHeight={44} + rowHeight={40} tableCellMaxWidth="100%" setSelected={handleSelectChange} page={currentPage} diff --git a/client/src/styles/theme.ts b/client/src/styles/theme.ts index 45554f28..3ce795d5 100644 --- a/client/src/styles/theme.ts +++ b/client/src/styles/theme.ts @@ -292,7 +292,7 @@ export const getAttuTheme = (mode: PaletteMode) => { }, MuiButton: { defaultProps: { - disableRipple: true, + disableRipple: false, }, styleOverrides: { root: ({ @@ -305,6 +305,7 @@ export const getAttuTheme = (mode: PaletteMode) => { padding: theme.spacing(1, 3), textTransform: 'initial', fontWeight: 'bold', + transition: 'all 0.2s ease-in-out', ...(ownerState.variant === 'text' && { padding: theme.spacing(1), color: theme.palette.primary.main, @@ -312,6 +313,9 @@ export const getAttuTheme = (mode: PaletteMode) => { backgroundColor: theme.palette.primary.main, color: theme.palette.background.paper, }, + '&:active': { + backgroundColor: theme.palette.primary.dark, + }, }), ...(ownerState.variant === 'contained' && { boxShadow: 'none', @@ -322,10 +326,27 @@ export const getAttuTheme = (mode: PaletteMode) => { ? theme.palette.secondary.dark : theme.palette.primary.dark, }, + '&:active': { + backgroundColor: + ownerState.color === 'secondary' + ? theme.palette.secondary.dark + : theme.palette.primary.dark, + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + }, + }), + ...(ownerState.variant === 'outlined' && { + '&:hover': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.background.paper, + }, + '&:active': { + backgroundColor: theme.palette.primary.dark, + }, }), ...(ownerState.disabled && { pointerEvents: 'none', opacity: 0.6, + transform: 'none !important', }), }), },