diff --git a/src/components/ExploreCubes/index.module.less b/src/components/ExploreCubes/index.module.less index 55f4328d..c3e1e989 100644 --- a/src/components/ExploreCubes/index.module.less +++ b/src/components/ExploreCubes/index.module.less @@ -47,6 +47,15 @@ background: rgba(71, 13, 105, 0.1); } +.panelSelected :global(.ant-collapse-header) { + background: rgba(163, 27, 203, 0.15) !important; + border-left: 3px solid rgba(163, 27, 203, 0.7); +} + +.panelSelected :global(.ant-collapse-header):hover { + background: rgba(163, 27, 203, 0.2) !important; +} + .panel :global(.ant-collapse-item) { border: 0 !important; } diff --git a/src/components/ExploreCubes/index.tsx b/src/components/ExploreCubes/index.tsx index 18ebaade..03721c25 100644 --- a/src/components/ExploreCubes/index.tsx +++ b/src/components/ExploreCubes/index.tsx @@ -98,7 +98,10 @@ const ExploreCubes: FC = ({ {cube} } - className={styles.panel} + className={cn( + styles.panel, + cubeSelectedCount > 0 && styles.panelSelected + )} extra={ = (props) => { const { order, hitLimit, - limit = 1000, + limit = 100, error, rows, columns, @@ -219,13 +219,13 @@ const ExploreDataSection: FC = (props) => { className={s.table} settings={settings} rowHeight={rowHeight} - footer={(tableRows) => ( -
- {t("data_section.shown")}: {tableRows.length} / {limit},{" "} + toolbarExtra={ + + {t("data_section.shown")}: {rows.length} / {limit},{" "} {t("data_section.offset")}: {offset}, {t("data_section.columns")}:{" "} {columns.length} -
- )} + + } /> ); }, [ diff --git a/src/components/ExploreSettingsForm/index.tsx b/src/components/ExploreSettingsForm/index.tsx index 2d938e60..f4625188 100644 --- a/src/components/ExploreSettingsForm/index.tsx +++ b/src/components/ExploreSettingsForm/index.tsx @@ -24,7 +24,7 @@ interface ExploreSettingsFormProps { const ExploreSettingsForm: FC = ({ defaultValues = { - limit: 1000, + limit: 100, offset: 0, } as DataSchemaFormValues, onChange, diff --git a/src/components/ModelsSidebar/index.module.less b/src/components/ModelsSidebar/index.module.less index fc17d53b..c3ba455d 100644 --- a/src/components/ModelsSidebar/index.module.less +++ b/src/components/ModelsSidebar/index.module.less @@ -96,6 +96,15 @@ } } +.fileBtnActive { + background: rgba(163, 27, 203, 0.15); + border-left: 3px solid rgba(163, 27, 203, 0.7); + + .fileControls { + opacity: 1; + } +} + .fileControls { opacity: 0; transition: 0.25s ease-in-out; diff --git a/src/components/ModelsSidebar/index.tsx b/src/components/ModelsSidebar/index.tsx index 2db60352..6e863f9e 100644 --- a/src/components/ModelsSidebar/index.tsx +++ b/src/components/ModelsSidebar/index.tsx @@ -46,6 +46,7 @@ export interface ModelsSidebarProps { editFile: Partial ) => void; onCreateFile: (values: Partial) => void; + activeFile?: string | null; dataSources: DataSourceInfo[]; onDataSourceChange: (dataSource: DataSourceInfo | null) => void; versionsCount?: number; @@ -73,6 +74,7 @@ const ModelsSidebar: FC = ({ onDeleteBranch, onSchemaDelete, onSchemaUpdate, + activeFile, dataSources, onDataSourceChange, dataSourceId, @@ -195,7 +197,10 @@ const ModelsSidebar: FC = ({ return (
onSelectFile(f.name)} > diff --git a/src/components/Sidebar/index.module.less b/src/components/Sidebar/index.module.less index 341c90f0..8a870e7f 100644 --- a/src/components/Sidebar/index.module.less +++ b/src/components/Sidebar/index.module.less @@ -1,5 +1,15 @@ +.resizableWrapper { + position: relative; + display: flex; + flex-shrink: 0; + height: 100%; +} + .wrapper { - width: 282px; + flex: 1; + min-width: 0; + height: 100%; + overflow: hidden; background: #f9f9f9; border-right: 1px solid rgba(0, 0, 0, 0.1); @@ -24,6 +34,8 @@ .body { background: #f9f9f9; + overflow-y: auto; + height: calc(100% - 60px); } .iconContainer { @@ -33,3 +45,20 @@ width: 25px; height: 25px; } + +.resizeHandle { + position: absolute; + top: 0; + right: -3px; + width: 6px; + height: 100%; + cursor: col-resize; + background: transparent; + transition: background 0.2s; + z-index: 10; + + &:hover, + &:active { + background: rgba(163, 27, 203, 0.3); + } +} diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 0ec11e6e..a82f2655 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -1,3 +1,4 @@ +import { useState, useCallback, useEffect, useRef } from "react"; import { useResponsive } from "ahooks"; import cn from "classnames"; @@ -5,6 +6,10 @@ import styles from "./index.module.less"; import type { FC, ReactNode } from "react"; +const DEFAULT_WIDTH = 282; +const MIN_WIDTH = 200; +const MAX_WIDTH = 500; + interface SidebarProps { icon?: ReactNode; title: ReactNode; @@ -13,16 +18,64 @@ interface SidebarProps { const Sidebar: FC = ({ icon, title, children }) => { const responsive = useResponsive(); + const [width, setWidth] = useState(DEFAULT_WIDTH); + const dragRef = useRef<{ startX: number; startWidth: number } | null>(null); + + const onMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + dragRef.current = { startX: e.clientX, startWidth: width }; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [width] + ); + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!dragRef.current) return; + const newWidth = Math.min( + MAX_WIDTH, + Math.max( + MIN_WIDTH, + dragRef.current.startWidth + e.clientX - dragRef.current.startX + ) + ); + setWidth(newWidth); + }; + const onMouseUp = () => { + if (dragRef.current) { + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + dragRef.current = null; + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + }, []); + + if (!responsive.lg) { + return ( +
+ {children &&
{children}
} +
+ ); + } return ( -
- {responsive.lg && ( +
+
{icon &&
{icon}
}
{title}
- )} - {children &&
{children}
} + {children &&
{children}
} +
+
); }; diff --git a/src/components/VirtualTable/index.module.less b/src/components/VirtualTable/index.module.less index b42c2ebf..b148a2c1 100644 --- a/src/components/VirtualTable/index.module.less +++ b/src/components/VirtualTable/index.module.less @@ -1,3 +1,10 @@ +.toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + .tableWrapper { width: 100%; overflow: auto; @@ -63,12 +70,33 @@ white-space: nowrap; text-align: left; text-overflow: ellipsis; + position: relative; } .headerParagraph { margin-bottom: 0 !important; font-weight: 700; font-size: 12px; + flex: 1; + min-width: 0; +} + +.columnResizeHandle { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 8px; + cursor: col-resize; + color: rgba(0, 0, 0, 0.25); + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + + &:hover { + color: rgba(163, 27, 203, 0.6); + } } .table :global(.ReactVirtualized__Table__row) { @@ -84,9 +112,14 @@ padding: 13px 0px; } +.table :global(.ReactVirtualized__Table__rowColumn:last-of-type), +.table :global(.ReactVirtualized__Table__headerColumn:last-of-type) { + padding-right: 16px; +} + .table :global(.ReactVirtualized__Table__rowColumn:first-of-type), .table :global(.ReactVirtualized__Table__headerColumn:first-of-type) { - margin-left: 18px; + margin-left: 8px; } .table :global(.ReactVirtualized__Table__Grid) { @@ -95,7 +128,14 @@ } .indexColumn { - color: var(--color-black); + color: #000 !important; + overflow: visible !important; +} + +.indexCell { + color: #000 !important; + font-weight: 600; + font-size: 12px; } // .table :global(.ReactVirtualized__Table__row:nth-child(even)) { diff --git a/src/components/VirtualTable/index.tsx b/src/components/VirtualTable/index.tsx index bbbbeb95..e6d91b24 100644 --- a/src/components/VirtualTable/index.tsx +++ b/src/components/VirtualTable/index.tsx @@ -1,12 +1,14 @@ -import { useMemo } from "react"; +import { useMemo, useState, useCallback, useEffect, useRef } from "react"; import { + ColumnWidthOutlined, + HolderOutlined, MoreOutlined, SortAscendingOutlined, SortDescendingOutlined, } from "@ant-design/icons"; import { getOr } from "unchanged"; import cn from "classnames"; -import { Alert, Empty, Spin, Tooltip, Typography, message } from "antd"; +import { Alert, Button, Empty, Spin, Tooltip, Typography, message } from "antd"; import { useTable, useSortBy } from "react-table"; import copy from "copy-to-clipboard"; import { @@ -38,7 +40,10 @@ import type { FC, ReactNode } from "react"; import type { MenuProps } from "antd"; const COL_WIDTH = 200; -const INDEX_COL_WIDTH = 50; +const INDEX_COL_WIDTH = 70; +const MIN_COL_WIDTH = 100; +const PX_PER_CHAR = 8; +const HEADER_PADDING = 32; // set with unique ids inside https://stackoverflow.com/a/49821454 export class SortBySet extends Set { @@ -136,6 +141,8 @@ interface VirtualTableProps { className?: string; settings?: QuerySettings; sortinMode?: "client-side" | "server-side"; + showAutoSizeButton?: boolean; + toolbarExtra?: ReactNode; } const VirtualTable: FC = ({ @@ -164,7 +171,61 @@ const VirtualTable: FC = ({ loading, loadingTip, sortinMode = "client-side", + showAutoSizeButton = true, + toolbarExtra, }) => { + const [columnWidths, setColumnWidths] = useState>({}); + const [tableKey, setTableKey] = useState(0); + const resizeRef = useRef<{ + colId: string; + startX: number; + startWidth: number; + } | null>(null); + + const getColumnWidth = useCallback( + (colId: string) => + Math.max(MIN_COL_WIDTH, columnWidths[colId] ?? COL_WIDTH), + [columnWidths] + ); + + const onResizeStart = useCallback( + (colId: string, e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + resizeRef.current = { + colId, + startX: e.clientX, + startWidth: getColumnWidth(colId), + }; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [getColumnWidth] + ); + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!resizeRef.current) return; + const { colId, startX, startWidth } = resizeRef.current; + const newWidth = Math.max(MIN_COL_WIDTH, startWidth + e.clientX - startX); + setColumnWidths((prev) => ({ ...prev, [colId]: newWidth })); + }; + const onMouseUp = () => { + if (resizeRef.current) { + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + setTableKey((k) => k + 1); + } + resizeRef.current = null; + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + }, []); + const defaultColumns = useMemo( () => Object.keys(getOr({}, 0, data)).map((colId) => { @@ -196,21 +257,49 @@ const VirtualTable: FC = ({ useSortBy ); + const autoSizeColumns = useCallback(() => { + if (flatHeaders.length === 0) return; + const HEADER_EXTRA = 48; + const newWidths: Record = {}; + const rawRows = data || []; + const hasNoRows = rawRows.length === 0; + flatHeaders.forEach((col) => { + const headerStr = typeof col.Header === "string" ? col.Header : col.id; + const headerWidth = + String(headerStr).length * PX_PER_CHAR + HEADER_PADDING + HEADER_EXTRA; + let maxDataWidth = 0; + rawRows.forEach((row: any) => { + const val = row?.[col.id]; + const str = + typeof val === "object" ? JSON.stringify(val) : String(val ?? ""); + const len = Math.min(str.length, 100); + maxDataWidth = Math.max( + maxDataWidth, + len * PX_PER_CHAR + HEADER_PADDING + ); + }); + const baseWidth = Math.max(headerWidth, maxDataWidth); + newWidths[col.id] = Math.max( + MIN_COL_WIDTH, + hasNoRows ? Math.ceil(headerWidth * 1.15) : baseWidth + ); + }); + setColumnWidths(newWidths); + setTableKey((k) => k + 1); + message.success("Columns auto-sized"); + }, [flatHeaders, data]); + const headerRenderer: TableHeaderRenderer = ({ label, columnData }) => { const { sortDirection, onSortChange, columnId, granularity } = columnData; - let humanLabel = label; - - if (granularity) { - humanLabel = `${label} (by ${granularity})`; - } + const fullTitle = (columnData as any).fullTitle; + const tooltipTitle = + fullTitle ?? (granularity ? `${label} (by ${granularity})` : label); + const shortLabel = typeof label === "string" ? label : String(label ?? ""); const children = [ - + - {humanLabel} + {shortLabel} , ]; @@ -248,22 +337,32 @@ const VirtualTable: FC = ({ }, ]; - if (sortDisabled) { - return children; + if (!sortDisabled) { + children.push( + + ); } + const dataKey = (columnData as any).dataKey ?? columnId; children.push( - +
onResizeStart(dataKey, e)} + > + +
); return children; @@ -344,14 +443,10 @@ const VirtualTable: FC = ({ const isEmpty = !columns.length && !rows.length; const tableWidth = useMemo(() => { - let tw = flatHeaders.length * COL_WIDTH; - - if (!hideIndexColumn) { - tw += INDEX_COL_WIDTH; - } - + let tw = flatHeaders.reduce((sum, col) => sum + getColumnWidth(col.id), 0); + if (!hideIndexColumn) tw += INDEX_COL_WIDTH; return tw; - }, [flatHeaders.length, hideIndexColumn]); + }, [flatHeaders, getColumnWidth, hideIndexColumn]); const defaultEmptyComponent = ( @@ -378,8 +473,24 @@ const VirtualTable: FC = ({ height: height + 10, }} > + {(showAutoSizeButton || toolbarExtra) && ( +
+ {showAutoSizeButton && ( + + )} + {toolbarExtra} +
+ )}
= ({ rowCount={rows.length} rowGetter={({ index }) => rows[index]} rowStyle={({ index }) => ({ - width: tableWidth, background: index % 2 ? "rgba(249, 249, 249, 1)" : "none", })} noRowsRenderer={noRowsRenderer} @@ -401,10 +511,15 @@ const VirtualTable: FC = ({ {!hideIndexColumn && ( rowData.index + 1} + cellRenderer={({ cellData }) => ( +
{cellData}
+ )} dataKey="index" width={INDEX_COL_WIDTH} + flexGrow={0} + flexShrink={0} /> )} {flatHeaders.map((col) => { @@ -412,6 +527,7 @@ const VirtualTable: FC = ({ const columnMemberId = `${cube}.${field}`; const value = col.render("Header"); + const fullTitle = (col as any).fullTitle ?? value; const colSortConfig = sortBy.find((sortItem) => sortItem.id === columnMemberId) || @@ -429,12 +545,16 @@ const VirtualTable: FC = ({ key={col.id} label={value} dataKey={col.id} - width={COL_WIDTH} + width={getColumnWidth(col.id)} + flexGrow={0} + flexShrink={0} headerRenderer={headerRenderer} cellDataGetter={cellDataGetter} cellRenderer={internalCellRenderer} columnData={{ columnId: columnMemberId, + dataKey: col.id, + fullTitle, onSortChange, sortDirection, granularity, diff --git a/src/hooks/useAnalyticsQuery.ts b/src/hooks/useAnalyticsQuery.ts index 23a8b17b..dae657ec 100644 --- a/src/hooks/useAnalyticsQuery.ts +++ b/src/hooks/useAnalyticsQuery.ts @@ -152,7 +152,7 @@ export const queryState: PlaygroundState = { ...queryBaseMembers, order: [], timezone: "UTC", - limit: 1000, + limit: 100, offset: 0, }; diff --git a/src/hooks/usePlayground.ts b/src/hooks/usePlayground.ts index 5f4d8644..64f71042 100644 --- a/src/hooks/usePlayground.ts +++ b/src/hooks/usePlayground.ts @@ -10,7 +10,6 @@ import useExplorationData from "@/hooks/useExplorationData"; import pickKeys from "@/utils/helpers/pickKeys"; import equals from "@/utils/helpers/equals"; import type { CubeMembers } from "@/types/cube"; -import { getTitle } from "@/utils/helpers/getTitles"; import type { QuerySettings } from "@/types/querySettings"; import type { ExplorationData, @@ -68,7 +67,7 @@ const reducer: Reducer = ( return state; }; -export const getColumns = (selectedQueryMembers: CubeMembers, settings = {}) => +export const getColumns = (selectedQueryMembers: CubeMembers) => [ ...Object.values(selectedQueryMembers.dimensions || {}).map((d) => ({ ...d, @@ -77,7 +76,8 @@ export const getColumns = (selectedQueryMembers: CubeMembers, settings = {}) => ...Object.values(selectedQueryMembers.measures || {}), ].map((c) => ({ id: c.name, - Header: getTitle(settings, c), + Header: c.shortTitle ?? c.title ?? c.name, + fullTitle: c.title ?? c.name, accessor: (row: any) => row[c.name], colId: c.name, type: c.type, @@ -131,8 +131,8 @@ export default ({ meta = [], explorationData, rawSql }: Props) => { return []; } - return getColumns(selectedQueryMembers, settings); - }, [selectedQueryMembers, settings]); + return getColumns(selectedQueryMembers); + }, [selectedQueryMembers]); const explorationState: ExplorationState = useMemo( () => ({ diff --git a/src/pages/Models/index.tsx b/src/pages/Models/index.tsx index 9145291e..4da70aa3 100644 --- a/src/pages/Models/index.tsx +++ b/src/pages/Models/index.tsx @@ -234,6 +234,7 @@ export const Models: React.FC = ({ onCreateBranch={onCreateBranch} onCreateFile={onSchemaCreate} onSelectFile={openSchema} + activeFile={activeTab} dataSources={dataSources || []} dataSourceId={dataSource?.id} versionsCount={versionsCount}