Skip to content

Commit ab1bc46

Browse files
committedMar 24, 2025
feat(web-console): persist expand/collapse states for table listing
1 parent 95d4837 commit ab1bc46

File tree

5 files changed

+125
-34
lines changed

5 files changed

+125
-34
lines changed
 

‎packages/web-console/src/scenes/Schema/Row/index.tsx

+12-14
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
import React, { MouseEvent, useContext, useState, useEffect, useRef } from "react"
2626
import styled from "styled-components"
27-
import { Rocket } from "@styled-icons/boxicons-regular"
27+
import { Rocket, InfoCircle } from "@styled-icons/boxicons-regular"
2828
import { SortDown } from "@styled-icons/boxicons-regular"
2929
import { ChevronRight } from "@styled-icons/boxicons-solid"
3030
import { Error as ErrorIcon } from "@styled-icons/boxicons-regular"
@@ -34,7 +34,7 @@ import { OneHundredTwentyThree, CalendarMinus, Globe, GeoAlt, Type as CharIcon }
3434
import type { TreeNodeKind } from "../../../components/Tree"
3535
import * as QuestDB from "../../../utils/questdb"
3636
import Highlighter from "react-highlight-words"
37-
import { TableIcon, MaterializedViewIcon } from "../table-icon"
37+
import { TableIcon } from "../table-icon"
3838
import { Box } from "@questdb/react-components"
3939
import { Text, TransitionDuration, IconWithTooltip, spinAnimation } from "../../../components"
4040
import { color } from "../../../utils"
@@ -111,6 +111,10 @@ const StyledTitle = styled(Title)`
111111
background-color: #45475a;
112112
color: ${({ theme }) => theme.color.foreground};
113113
}
114+
115+
svg {
116+
color: ${color("cyan")};
117+
}
114118
`
115119

116120
const TableActions = styled.span`
@@ -133,11 +137,13 @@ const Spacer = styled.span`
133137
const RocketIcon = styled(Rocket)`
134138
color: ${color("orange")};
135139
margin-right: 1rem;
140+
flex-shrink: 0;
136141
`
137142

138143
const SortDownIcon = styled(SortDown)`
139144
color: ${color("green")};
140145
margin-right: 0.8rem;
146+
flex-shrink: 0;
141147
`
142148

143149
const ChevronRightIcon = styled(ChevronRight)`
@@ -157,17 +163,6 @@ const DotIcon = styled(CheckboxBlankCircle)`
157163
margin-right: 1rem;
158164
`
159165

160-
const TruncatedBox = styled(Box)`
161-
display: inline;
162-
text-overflow: ellipsis;
163-
overflow: hidden;
164-
white-space: nowrap;
165-
cursor: default;
166-
font-style: italic;
167-
color: ${color("gray2")};
168-
text-align: right;
169-
`
170-
171166
const Loader = styled(Loader4)`
172167
margin-left: 1rem;
173168
color: ${color("orange")};
@@ -366,6 +361,9 @@ const Row = ({
366361
isMaterializedView={kind === "matview"}
367362
/>
368363
)}
364+
{kind === "detail" && (
365+
<InfoCircle size="14px" />
366+
)}
369367
<Highlighter
370368
highlightClassName="highlight"
371369
searchWords={[query ?? ""]}
@@ -380,7 +378,7 @@ const Row = ({
380378
)}
381379

382380
{kind === "detail" && (
383-
<Text color="gray2" transform="lowercase">
381+
<Text color="gray2">
384382
{value}
385383
</Text>
386384
)}

‎packages/web-console/src/scenes/Schema/Table/index.tsx

+46-12
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import styled from "styled-components"
2727
import { Tree } from "../../../components"
2828
import { TreeNode, TreeNodeRenderParams, Text } from "../../../components"
2929
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, MenuItem } from "../../../components/ContextMenu"
30-
import { color } from "../../../utils"
3130
import * as QuestDB from "../../../utils/questdb"
3231
import Row from "../Row"
3332
import { useDispatch } from "react-redux"
@@ -37,6 +36,7 @@ import { NotificationType } from "../../../types"
3736
import { TreeNodeKind } from "../../../components/Tree"
3837
import { SuspensionDialog } from '../SuspensionDialog'
3938
import { FileCopy, Restart } from "@styled-icons/remix-line"
39+
import { getTableExpanded, setMatViewExpanded, setTableExpanded, getFolderExpanded, setFolderExpanded } from "../localStorageUtils"
4040

4141
type Props = QuestDB.Table &
4242
Readonly<{
@@ -47,8 +47,11 @@ type Props = QuestDB.Table &
4747
walTableData?: QuestDB.WalTable
4848
matViewData?: QuestDB.MaterializedView
4949
selected: boolean
50+
onSelectToggle: ({name, type}: {name: string, type: TreeNodeKind}) => void
5051
selectOpen: boolean
51-
onSelectToggle: ({name, type}: {name: string, type: TreeNodeKind}) => void
52+
cachedColumns?: QuestDB.Column[]
53+
onCacheColumns?: (columns: QuestDB.Column[]) => void
54+
onClearColumnsCache?: (tableName: string) => void
5255
}>
5356

5457
const Title = styled(Row)`
@@ -104,18 +107,29 @@ const Table = ({
104107
matViewData,
105108
dedup,
106109
selected,
107-
selectOpen,
108110
onSelectToggle,
111+
selectOpen,
109112
matView,
113+
cachedColumns,
114+
onCacheColumns,
115+
onClearColumnsCache,
110116
}: Props) => {
111117
const { quest } = useContext(QuestContext)
112118
const dispatch = useDispatch()
113119
const [suspensionDialogOpen, setSuspensionDialogOpen] = useState(false)
114120

115121
const showColumns = async (name: string) => {
116122
try {
123+
if (cachedColumns) {
124+
return {
125+
type: QuestDB.Type.DQL,
126+
data: cachedColumns
127+
};
128+
}
129+
117130
const response = await quest.showColumns(name)
118131
if (response && response.type === QuestDB.Type.DQL) {
132+
onCacheColumns?.(response.data);
119133
return response
120134
}
121135
} catch (error: any) {
@@ -164,6 +178,7 @@ const Table = ({
164178
{
165179
name: table_name,
166180
kind: matView ? 'matview' : 'table',
181+
initiallyOpen: getTableExpanded(table_name),
167182
render: ({ toggleOpen, isOpen, isLoading }) => {
168183
return (
169184
<ContextMenu>
@@ -174,7 +189,15 @@ const Table = ({
174189
table_id={id}
175190
name={table_name}
176191
baseTable={matViewData?.base_table_name}
177-
onClick={toggleOpen}
192+
onClick={() => {
193+
toggleOpen()
194+
if (matView) {
195+
setMatViewExpanded(table_name, !isOpen)
196+
} else {
197+
setTableExpanded(table_name, !isOpen)
198+
}
199+
onClearColumnsCache?.(table_name);
200+
}}
178201
isLoading={isLoading}
179202
selectOpen={selectOpen}
180203
selected={selected}
@@ -198,7 +221,6 @@ const Table = ({
198221
{walTableData?.suspended && (
199222
<MenuItem
200223
data-hook="table-context-menu-resume-wal"
201-
// Suspension dialog & context menu modifies pointer events -- to prevent a race condition
202224
onClick={() => setTimeout(() => setSuspensionDialogOpen(true))}
203225
icon={<Restart size={14} />}
204226
>
@@ -212,7 +234,7 @@ const Table = ({
212234
async onOpen({ setChildren }) {
213235
const columns: TreeNode = {
214236
name: "Columns",
215-
initiallyOpen: true,
237+
initiallyOpen: getFolderExpanded(matView ? 'matview' : 'table', table_name, "Columns"),
216238
async onOpen({ setChildren }) {
217239
const response = await showColumns(table_name)
218240

@@ -243,21 +265,26 @@ const Table = ({
243265
kind="folder"
244266
table_id={id}
245267
name="Columns"
246-
onClick={() => toggleOpen()}
268+
onClick={() => {
269+
setFolderExpanded(matView ? 'matview' : 'table', table_name, "Columns", isOpen ? false : true);
270+
onClearColumnsCache?.(table_name);
271+
toggleOpen();
272+
}}
247273
isLoading={isLoading}
248274
/>
249275
)
250276
},
251277
}
252278

253279
const storageDetails: TreeNode = {
254-
name: 'Storage Details',
280+
name: 'Storage details',
255281
kind: 'folder',
282+
initiallyOpen: getFolderExpanded(matView ? 'matview' : 'table', table_name, "Storage details"),
256283
async onOpen({ setChildren }) {
257284
const details = [
258285
{
259-
name: 'WAL Enabled',
260-
value: walEnabled ? 'Yes' : 'No',
286+
name: 'WAL',
287+
value: walEnabled ? 'Enabled' : 'Disabled',
261288
},
262289
{
263290
name: 'Partitioning',
@@ -276,7 +303,10 @@ const Table = ({
276303
expanded={isOpen && !isLoading}
277304
table_id={id}
278305
name="Storage details"
279-
onClick={() => toggleOpen()}
306+
onClick={() => {
307+
setFolderExpanded(matView ? 'matview' : 'table', table_name, "Storage details", isOpen ? false : true);
308+
toggleOpen();
309+
}}
280310
isLoading={isLoading}
281311
/>
282312
)
@@ -286,6 +316,7 @@ const Table = ({
286316
const baseTables: TreeNode[] = matViewData ? [{
287317
name: 'Base tables',
288318
kind: 'folder',
319+
initiallyOpen: getFolderExpanded("matview", table_name, "Base tables"),
289320
async onOpen({ setChildren }) {
290321
setChildren([{
291322
name: matViewData.base_table_name,
@@ -299,7 +330,10 @@ const Table = ({
299330
expanded={isOpen && !isLoading}
300331
table_id={id}
301332
name="Base tables"
302-
onClick={() => toggleOpen()}
333+
onClick={() => {
334+
setFolderExpanded("matview", table_name, "Base tables", isOpen ? false : true);
335+
toggleOpen();
336+
}}
303337
isLoading={isLoading}
304338
/>
305339
)

‎packages/web-console/src/scenes/Schema/VirtualTables/index.tsx

+26-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC, useEffect, useMemo, useState } from 'react';
1+
import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react';
22
import { GroupedVirtuoso } from 'react-virtuoso';
33
import styled from 'styled-components';
44
import { Loader3 } from '@styled-icons/remix-line';
@@ -7,9 +7,10 @@ import { color, ErrorResult } from '../../../utils';
77
import * as QuestDB from "../../../utils/questdb";
88
import { State, View } from "../../Schema";
99
import Table from "../Table";
10-
import LoadingError from "../LoadingError"
10+
import LoadingError from "../LoadingError";
1111
import Row from "../Row";
1212
import { TreeNodeKind } from "../../../components/Tree";
13+
import { getSectionExpanded, setSectionExpanded, TABLES_GROUP_KEY, MATVIEWS_GROUP_KEY } from "../localStorageUtils";
1314

1415
type VirtualTablesProps = {
1516
tables: QuestDB.Table[]
@@ -24,6 +25,10 @@ type VirtualTablesProps = {
2425
loadingError: ErrorResult | null
2526
}
2627

28+
type ColumnsCache = {
29+
[tableName: string]: QuestDB.Column[];
30+
};
31+
2732
const SectionHeader = styled(Row)<{ $disabled: boolean }>`
2833
cursor: ${({ $disabled }) => $disabled ? 'not-allowed' : 'pointer'};
2934
pointer-events: ${({ $disabled }) => $disabled ? 'none' : 'auto'};
@@ -71,8 +76,16 @@ export const VirtualTables: FC<VirtualTablesProps> = ({
7176
state,
7277
loadingError
7378
}) => {
74-
const [tablesExpanded, setTablesExpanded] = useState(true)
75-
const [matViewsExpanded, setMatViewsExpanded] = useState(false)
79+
const columnsCache = useRef<ColumnsCache>({});
80+
81+
const [, setToggle] = useState(false);
82+
const forceUpdate = () => setToggle(toggle => !toggle);
83+
const tablesExpanded = getSectionExpanded(TABLES_GROUP_KEY)
84+
const matViewsExpanded = getSectionExpanded(MATVIEWS_GROUP_KEY)
85+
86+
const clearColumnsCache = useCallback((tableName: string) => {
87+
delete columnsCache.current[tableName];
88+
}, []);
7689

7790
const { groups, groupCounts, allTables } = useMemo(() => {
7891
const filtered = tables.filter((table: QuestDB.Table) => {
@@ -128,6 +141,9 @@ export const VirtualTables: FC<VirtualTablesProps> = ({
128141
<GroupedVirtuoso
129142
groupCounts={groupCounts}
130143
components={{ TopItemList: React.Fragment }}
144+
overscan={200}
145+
defaultItemHeight={60}
146+
increaseViewportBy={{ top: 300, bottom: 300 }}
131147
groupContent={index => {
132148
const group = groups[index]
133149

@@ -144,8 +160,8 @@ export const VirtualTables: FC<VirtualTablesProps> = ({
144160
return `${matViewsExpanded ? 'collapse' : 'expand'}-materialized-views`
145161
})()}
146162
onClick={() => {
147-
if (index === 0) setTablesExpanded(!tablesExpanded)
148-
else setMatViewsExpanded(!matViewsExpanded)
163+
setSectionExpanded(index === 0 ? TABLES_GROUP_KEY : MATVIEWS_GROUP_KEY, !group.expanded);
164+
forceUpdate();
149165
}}
150166
/>
151167
)
@@ -162,7 +178,7 @@ export const VirtualTables: FC<VirtualTablesProps> = ({
162178
t.name === table.table_name
163179
&& t.type === (table.matView ? "matview" : "table")
164180
))
165-
181+
166182
return (
167183
<Table
168184
matView={table.matView}
@@ -182,6 +198,9 @@ export const VirtualTables: FC<VirtualTablesProps> = ({
182198
selectOpen={selectOpen}
183199
selected={selected}
184200
onSelectToggle={handleSelectToggle}
201+
cachedColumns={columnsCache.current[table.table_name]}
202+
onCacheColumns={(columns) => columnsCache.current[table.table_name] = columns}
203+
onClearColumnsCache={clearColumnsCache}
185204
/>
186205
)
187206
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const STORAGE_KEY_PREFIX = 'questdb:expanded:';
2+
export const TABLES_GROUP_KEY = `${STORAGE_KEY_PREFIX}tables`;
3+
export const MATVIEWS_GROUP_KEY = `${STORAGE_KEY_PREFIX}matviews`;
4+
5+
const getItemFromStorage = (key: string, defaultValue = false): boolean => {
6+
try {
7+
const value = localStorage.getItem(key);
8+
return value === null ? defaultValue : value === 'true';
9+
} catch (e) {
10+
return defaultValue;
11+
}
12+
};
13+
14+
const setItemToStorage = (key: string, value: boolean): void => {
15+
try {
16+
localStorage.setItem(key, value ? 'true' : 'false');
17+
if (!value) {
18+
Object.keys(localStorage).filter(k => k.startsWith(key)).forEach(k => localStorage.removeItem(k));
19+
}
20+
} catch (e) {
21+
console.warn('Failed to save to localStorage:', e);
22+
}
23+
};
24+
25+
export const getTableKey = (tableName: string): string => `${TABLES_GROUP_KEY}:${tableName}`;
26+
export const getMatViewKey = (tableName: string): string => `${MATVIEWS_GROUP_KEY}:${tableName}`;
27+
export const getFolderKey = (kind: 'table' | 'matview', tableName: string, folderName: string): string =>
28+
`${kind === 'table' ? TABLES_GROUP_KEY : MATVIEWS_GROUP_KEY}:${tableName}:${folderName.toLowerCase().replace(/\s+/g, '')}`;
29+
30+
export const getSectionExpanded = (sectionKey: string): boolean => getItemFromStorage(sectionKey);
31+
export const setSectionExpanded = (sectionKey: string, expanded: boolean): void => setItemToStorage(sectionKey, expanded)
32+
33+
export const getTableExpanded = (tableName: string): boolean => getItemFromStorage(getTableKey(tableName));
34+
export const setTableExpanded = (tableName: string, expanded: boolean): void => setItemToStorage(getTableKey(tableName), expanded);
35+
36+
export const getMatViewExpanded = (tableName: string): boolean => getItemFromStorage(getMatViewKey(tableName));
37+
export const setMatViewExpanded = (tableName: string, expanded: boolean): void => setItemToStorage(getMatViewKey(tableName), expanded);
38+
39+
export const getFolderExpanded = (kind: 'table' | 'matview', tableName: string, folderName: string): boolean => getItemFromStorage(getFolderKey(kind, tableName, folderName));
40+
export const setFolderExpanded = (kind: 'table' | 'matview', tableName: string, folderName: string, expanded: boolean): void => setItemToStorage(getFolderKey(kind, tableName, folderName), expanded);

‎packages/web-console/src/scenes/Schema/table-icon.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const Asterisk = styled.span`
3535

3636
const NonPartitionedTableIcon = ({ height = "14px", width = "14px" }) => (
3737
<svg viewBox="0 0 24 24" height={height} width={width} fill="currentColor" xmlns="http://www.w3.org/2000/svg">
38-
<path d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM4 8h16V5H4v3zM4 10h16v9H4v-9z" fill-rule="evenodd" clip-rule="evenodd"/>
38+
<path d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM4 8h16V5H4v3zM4 10h16v9H4v-9z" fillRule="evenodd" clipRule="evenodd"/>
3939
</svg>
4040
)
4141

0 commit comments

Comments
 (0)
Failed to load comments.