diff --git a/client/src/consts/index.ts b/client/src/consts/index.ts index fd4b7554..f7910b94 100644 --- a/client/src/consts/index.ts +++ b/client/src/consts/index.ts @@ -3,4 +3,5 @@ export * from './Localstorage'; export * from './Milvus'; export * from './Prometheus'; export * from './Util'; +export * from './ui'; export * from './default'; diff --git a/client/src/consts/ui.ts b/client/src/consts/ui.ts new file mode 100644 index 00000000..1b0b318c --- /dev/null +++ b/client/src/consts/ui.ts @@ -0,0 +1 @@ +export const DEFAULT_TREE_WIDTH = 230; diff --git a/client/src/context/Data.tsx b/client/src/context/Data.tsx index dea82964..9a07dfff 100644 --- a/client/src/context/Data.tsx +++ b/client/src/context/Data.tsx @@ -20,6 +20,7 @@ import { DatabaseObject, } from '@server/types'; import { WS_EVENTS, WS_EVENTS_TYPE, LOADING_STATE } from '@server/utils/Const'; +import { DEFAULT_TREE_WIDTH } from '@/consts'; import { checkIndexing, checkLoading } from '@server/utils/Shared'; export const dataContext = createContext({ @@ -67,6 +68,12 @@ export const dataContext = createContext({ setProperty: async () => { return {} as CollectionFullObject; }, + ui: { + tree: { + width: DEFAULT_TREE_WIDTH, + }, + }, + setUIPref: () => {}, }); const { Provider } = dataContext; @@ -76,6 +83,13 @@ export const DataProvider = (props: { children: React.ReactNode }) => { const { authReq, isAuth, clientId, logout, setAuthReq } = useContext(authContext); + // UI preferences + const [ui, setUI] = useState({ + tree: { + width: DEFAULT_TREE_WIDTH, + }, + }); + // local data state const [collections, setCollections] = useState([]); const [connected, setConnected] = useState(false); @@ -344,6 +358,26 @@ export const DataProvider = (props: { children: React.ReactNode }) => { return newCollection; }; + // set UI preferences + const setUIPref = (pref: DataContextType['ui']) => { + setUI(pref); + localStorage.setItem('attu.ui.tree.width', String(pref.tree.width)); + }; + + // load UI preferences + useEffect(() => { + const storedWidth = Number(localStorage.getItem('attu.ui.tree.width')); + if (storedWidth) { + setUI(prevUI => ({ + ...prevUI, + tree: { + ...prevUI.tree, + width: storedWidth, + }, + })); + } + }, []); + useEffect(() => { const clear = () => { // clear collections @@ -442,6 +476,8 @@ export const DataProvider = (props: { children: React.ReactNode }) => { createAlias, dropAlias, setProperty, + ui, + setUIPref, }} > {props.children} diff --git a/client/src/context/Types.ts b/client/src/context/Types.ts index de9717c0..05ab8f66 100644 --- a/client/src/context/Types.ts +++ b/client/src/context/Types.ts @@ -104,7 +104,6 @@ export type DataContextType = { setDatabase: Dispatch>; databases: DatabaseObject[]; setDatabaseList: Dispatch>; - // search UI state // APIs // databases @@ -142,4 +141,11 @@ export type DataContextType = { key: string, value: any ) => Promise; + // UI preferences + ui: { + tree: { + width: number; + }; + }; + setUIPref: (pref: DataContextType['ui']) => void; }; diff --git a/client/src/pages/databases/Databases.tsx b/client/src/pages/databases/Databases.tsx index 5f79ab2c..9936f2b3 100644 --- a/client/src/pages/databases/Databases.tsx +++ b/client/src/pages/databases/Databases.tsx @@ -27,6 +27,20 @@ const DEFAULT_TREE_WIDTH = 230; const useStyles = makeStyles((theme: Theme) => ({ wrapper: { flexDirection: 'row', + '&.dragging': { + cursor: 'ew-resize', + '& $tree': { + pointerEvents: 'none', + userSelect: 'none', + }, + '& $tab': { + pointerEvents: 'none', + userSelect: 'none', + }, + '& $dragger': { + background: theme.palette.divider, + }, + }, }, tree: { boxShadow: 'none', @@ -57,7 +71,6 @@ const useStyles = makeStyles((theme: Theme) => ({ background: theme.palette.divider, }, }, - tab: { flexGrow: 1, flexShrink: 1, @@ -76,7 +89,7 @@ const useStyles = makeStyles((theme: Theme) => ({ // Databases page(tree and tabs) const Databases = () => { // context - const { database, collections, loading, fetchCollection } = + const { database, collections, loading, fetchCollection, ui, setUIPref } = useContext(dataContext); // UI state @@ -85,51 +98,62 @@ const Databases = () => { ); // tree ref + const [isDragging, setIsDragging] = useState(false); const treeRef = useRef(null); const draggerRef = useRef(null); + // support dragging tree width useEffect(() => { - let isDragging = false; + // local tree width let treeWidth = 0; const handleMouseMove = (e: MouseEvent) => { requestAnimationFrame(() => { // get mouse position let mouseX = e.clientX - treeRef.current!.offsetLeft; + // set min and max width treeWidth = Math.max(1, Math.min(mouseX, DEFAULT_TREE_WIDTH)); // set tree width - treeRef.current!.style.width = `${treeWidth}px`; - draggerRef.current!.classList.toggle('tree-collapsed', true); + setUIPref({ tree: { width: treeWidth } }); + // set dragging true + setIsDragging(true); }); }; const handleMouseUp = () => { - isDragging = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); + // highlight dragger alwasy if width === 1 draggerRef.current!.classList.toggle('tree-collapsed', treeWidth === 1); + // set dragging true + setIsDragging(false); }; const handleMouseDown = (e: MouseEvent) => { const t = e.target as HTMLDivElement; if (t && t.dataset.id === 'dragger') { - isDragging = true; + // set dragging true + setIsDragging(true); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } }; + // add event listener document.addEventListener('mousedown', handleMouseDown); return () => { + // remove event listener document.removeEventListener('mousedown', handleMouseDown); + // set dragging false + setIsDragging(false); }; }, []); // double click on the dragger, recover default const handleDoubleClick = () => { - treeRef.current!.style.width = `${DEFAULT_TREE_WIDTH}px`; draggerRef.current!.classList.toggle('tree-collapsed', false); + setUIPref({ tree: { width: DEFAULT_TREE_WIDTH } }); }; // init search params @@ -259,8 +283,16 @@ const Databases = () => { // render return ( -
-
+
+
{loading ? ( ) : (