Skip to content

Commit d9950b5

Browse files
committed
feat: Allow exporting csvs for one table at a time
https://harperdb.atlassian.net/browse/STUDIO-635
1 parent f072967 commit d9950b5

File tree

3 files changed

+168
-106
lines changed

3 files changed

+168
-106
lines changed

src/features/instance/databases/components/DatabaseTableView.tsx

Lines changed: 80 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import { InstanceDatabaseMap } from '@/integrations/api/api.patch';
1616
import { useDeleteTableRecords } from '@/integrations/api/instance/database/deleteTableRecords';
1717
import { getDescribeTableQueryOptions } from '@/integrations/api/instance/database/getDescribeTable';
1818
import {
19+
getSearchByConditions,
1920
getSearchByConditionsOptions,
2021
SearchCondition,
2122
translateColumnFilterToSearchConditions,
2223
} from '@/integrations/api/instance/database/getSearchByConditions';
2324
import { getSearchByIdOptions } from '@/integrations/api/instance/database/getSearchById';
24-
import { getSearchByValueOptions } from '@/integrations/api/instance/database/getSearchByValue';
25+
import { getSearchByValue, getSearchByValueOptions } from '@/integrations/api/instance/database/getSearchByValue';
2526
import { useUpdateTableRecords } from '@/integrations/api/instance/database/updateTableRecords';
2627
import { useSetWatchedValue } from '@/lib/events/watcher';
2728
import { keyBy } from '@/lib/keyBy';
@@ -33,12 +34,13 @@ import { Row, VisibilityState } from '@tanstack/react-table';
3334
import {
3435
CircleCheckBigIcon,
3536
CircleIcon,
37+
CloudDownloadIcon,
38+
CloudUploadIcon,
3639
EllipsisIcon,
3740
ExternalLinkIcon,
3841
FunnelIcon,
3942
FunnelPlusIcon,
4043
FunnelXIcon,
41-
ImportIcon,
4244
PlusIcon,
4345
RefreshCwIcon,
4446
Trash2Icon,
@@ -150,54 +152,46 @@ export function DatabaseTableView({ instanceDatabaseMap, databaseName, tableName
150152
const useFilteredList = filtersToggled && !!appliedSearchConditions;
151153

152154
// Full list
153-
const {
154-
data: fullTableData,
155-
isFetching: tableDataFetching,
156-
} = useQuery(
157-
getSearchByValueOptions({
158-
...instanceParams,
159-
enabled: !useFilteredList && !!hashAttribute,
160-
databaseName,
161-
tableName,
162-
searchAttribute: hashAttribute,
163-
sort,
164-
pageSize,
165-
pageIndex,
166-
onlyIfCached,
167-
}),
168-
);
155+
const searchByValueParams = {
156+
...instanceParams,
157+
enabled: !useFilteredList && !!hashAttribute,
158+
databaseName,
159+
tableName,
160+
searchAttribute: hashAttribute,
161+
sort,
162+
pageSize,
163+
pageIndex,
164+
onlyIfCached,
165+
};
166+
const searchByValueOptions = getSearchByValueOptions(searchByValueParams);
167+
const { data: fullTableData, isFetching: tableDataFetching } = useQuery(searchByValueOptions);
169168

170169
// Filtered list
171-
const {
172-
data: filteredTableData,
173-
isFetching: tableConditionsDataFetching,
174-
} = useQuery(
175-
getSearchByConditionsOptions({
176-
...instanceParams,
177-
enabled: useFilteredList && !!hashAttribute,
178-
databaseName,
179-
tableName,
180-
conditions: appliedSearchConditions,
181-
sort,
182-
pageSize,
183-
pageIndex,
184-
onlyIfCached,
185-
}),
186-
);
170+
const searchByConditionsParams = {
171+
...instanceParams,
172+
enabled: useFilteredList && !!hashAttribute,
173+
databaseName,
174+
tableName,
175+
conditions: appliedSearchConditions,
176+
sort,
177+
pageSize,
178+
pageIndex,
179+
onlyIfCached,
180+
};
181+
const searchByConditionsOptions = getSearchByConditionsOptions(searchByConditionsParams);
182+
const { data: filteredTableData, isFetching: tableConditionsDataFetching } = useQuery(searchByConditionsOptions);
187183

188184
const tableData = useFilteredList ? filteredTableData : fullTableData;
189185
const isFetching = tableDataFetching || tableConditionsDataFetching;
190186

191187
// One by id
192-
const { data: searchByIdData } = useQuery(
193-
getSearchByIdOptions({
194-
...instanceParams,
195-
enabled: isEditModalOpen,
196-
databaseName: databaseName,
197-
tableName: tableName,
198-
ids: selectedIds,
199-
}),
200-
);
188+
const { data: searchByIdData } = useQuery(getSearchByIdOptions({
189+
...instanceParams,
190+
enabled: isEditModalOpen,
191+
databaseName: databaseName,
192+
tableName: tableName,
193+
ids: selectedIds,
194+
}));
201195

202196
const { mutate: updateTableRecords, isPending: isUpdateTableRecordsPending } = useUpdateTableRecords();
203197
const { mutate: deleteTableRecords, isPending: isDeleteTableRecordsPending } = useDeleteTableRecords();
@@ -208,6 +202,37 @@ export function DatabaseTableView({ instanceDatabaseMap, databaseName, tableName
208202
[queryClient, instanceParams.entityId, databaseName, tableName],
209203
);
210204

205+
const [isExportingCSV, setisExportingCSV] = useState(false);
206+
const onExportCSVClicked = useCallback(async () => {
207+
if (!hashAttribute) {
208+
return;
209+
}
210+
const id = toast.loading('Loading CSV...');
211+
setisExportingCSV(true);
212+
const allResultsAsCSV = {
213+
pageIndex: 0,
214+
pageSize: 1_000_000,
215+
headers: {
216+
Accept: 'text/csv',
217+
},
218+
};
219+
const response = await (
220+
useFilteredList
221+
? getSearchByConditions({ ...searchByConditionsParams, ...allResultsAsCSV })
222+
: getSearchByValue({ ...searchByValueParams, ...allResultsAsCSV })
223+
);
224+
toast.loading('Preparing CSV...', { id });
225+
const content = response.data as unknown as string;
226+
const blob = new Blob([content], { type: 'text/csv' });
227+
const url = URL.createObjectURL(blob);
228+
const downloadLink = document.createElement('a');
229+
downloadLink.href = url;
230+
downloadLink.setAttribute('download', `${databaseName}.${tableName}.${new Date().toISOString()}.csv`);
231+
downloadLink.click();
232+
toast.success('CSV Exported!', { id });
233+
setisExportingCSV(false);
234+
}, [databaseName, tableName, searchByValueOptions, searchByConditionsOptions]);
235+
211236
const onRecordUpdate = useCallback((data: Record<string, unknown>[]) => {
212237
updateTableRecords(
213238
{
@@ -307,12 +332,23 @@ export function DatabaseTableView({ instanceDatabaseMap, databaseName, tableName
307332
disabled={isImportCSVModalOpen}
308333
accessKey="c"
309334
>
310-
<ImportIcon />
335+
<CloudUploadIcon />
311336
<span>
312337
Import <u>C</u>SV
313338
</span>
314339
</Button>
315340
)}
341+
<Button
342+
variant="positiveOutline"
343+
onClick={onExportCSVClicked}
344+
disabled={isExportingCSV}
345+
accessKey="e"
346+
>
347+
<CloudDownloadIcon />
348+
<span>
349+
<u>E</u>xport CSV
350+
</span>
351+
</Button>
316352
</div>
317353

318354
<div className="flex space-x-2">

src/integrations/api/instance/database/getSearchByConditions.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface GetSearchByConditionsParams extends InstanceClientIdConfig {
1313
pageIndex: number;
1414
pageSize: number;
1515
onlyIfCached: boolean;
16+
headers?: Record<string, any>;
1617
}
1718

1819
type Comparator =
@@ -48,18 +49,18 @@ interface SearchByConditionsRequest {
4849
noCacheStore: boolean;
4950
}
5051

51-
export function getSearchByConditionsOptions({
52-
enabled,
53-
entityId,
54-
instanceClient,
55-
databaseName,
56-
tableName,
57-
conditions,
58-
sort,
59-
pageIndex,
60-
pageSize,
61-
onlyIfCached,
62-
}: GetSearchByConditionsParams) {
52+
export function getSearchByConditionsOptions(params: GetSearchByConditionsParams) {
53+
const {
54+
enabled,
55+
entityId,
56+
databaseName,
57+
tableName,
58+
conditions,
59+
sort,
60+
pageIndex,
61+
pageSize,
62+
onlyIfCached,
63+
} = params;
6364
// starts_with, equals, etc
6465
return queryOptions({
6566
enabled: enabled && !!conditions,
@@ -77,28 +78,40 @@ export function getSearchByConditionsOptions({
7778
] as const,
7879
staleTime: 60_000,
7980
gcTime: 5_000,
80-
8181
retry: false,
82-
queryFn: () =>
83-
instanceClient.post<Record<string, unknown>[]>(
84-
'/',
85-
{
86-
operation: 'search_by_conditions',
87-
get_attributes: ['*'],
88-
database: databaseName,
89-
table: tableName,
90-
conditions: conditions!,
91-
sort: sort.attribute.length ? sort : undefined,
92-
offset: pageIndex * pageSize,
93-
limit: pageSize,
94-
onlyIfCached: onlyIfCached,
95-
noCacheStore: onlyIfCached,
96-
} satisfies SearchByConditionsRequest,
97-
{ timeout: 0 },
98-
),
82+
queryFn: () => getSearchByConditions(params),
9983
});
10084
}
10185

86+
export function getSearchByConditions({
87+
instanceClient,
88+
databaseName,
89+
tableName,
90+
conditions,
91+
sort,
92+
pageIndex,
93+
pageSize,
94+
onlyIfCached,
95+
headers,
96+
}: GetSearchByConditionsParams) {
97+
return instanceClient.post<Record<string, unknown>[]>(
98+
'/',
99+
{
100+
operation: 'search_by_conditions',
101+
get_attributes: ['*'],
102+
database: databaseName,
103+
table: tableName,
104+
conditions: conditions!,
105+
sort: sort.attribute.length ? sort : undefined,
106+
offset: pageIndex * pageSize,
107+
limit: pageSize,
108+
onlyIfCached: onlyIfCached,
109+
noCacheStore: onlyIfCached,
110+
} satisfies SearchByConditionsRequest,
111+
{ timeout: 0, headers },
112+
);
113+
}
114+
102115
export function translateColumnFilterToSearchConditions(
103116
key: string,
104117
rawValues: string,

src/integrations/api/instance/database/getSearchByValue.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface GetSearchByValueParams extends InstanceClientIdConfig {
1010
pageIndex: number;
1111
pageSize: number;
1212
onlyIfCached: boolean;
13+
headers?: Record<string, any>;
1314
}
1415

1516
interface SearchByValueRequest {
@@ -26,18 +27,18 @@ interface SearchByValueRequest {
2627
noCacheStore: boolean;
2728
}
2829

29-
export function getSearchByValueOptions({
30-
enabled,
31-
entityId,
32-
instanceClient,
33-
databaseName,
34-
tableName,
35-
searchAttribute,
36-
sort,
37-
pageIndex,
38-
pageSize,
39-
onlyIfCached,
40-
}: GetSearchByValueParams) {
30+
export function getSearchByValueOptions(params: GetSearchByValueParams) {
31+
const {
32+
enabled,
33+
entityId,
34+
databaseName,
35+
tableName,
36+
searchAttribute,
37+
sort,
38+
pageIndex,
39+
pageSize,
40+
onlyIfCached,
41+
} = params;
4142
return queryOptions({
4243
enabled,
4344
queryKey: [
@@ -55,25 +56,37 @@ export function getSearchByValueOptions({
5556
retry: false,
5657
staleTime: 60_000,
5758
gcTime: 5_000,
58-
queryFn: () => {
59-
const customizedSort = sort.attribute.length && !(sort.attribute === searchAttribute && !sort.descending);
60-
return instanceClient.post<Record<string, unknown>[]>(
61-
'/',
62-
{
63-
operation: 'search_by_value',
64-
get_attributes: ['*'],
65-
database: databaseName,
66-
table: tableName,
67-
search_attribute: searchAttribute,
68-
search_value: '*',
69-
sort: customizedSort ? sort : undefined,
70-
offset: pageIndex * pageSize,
71-
limit: pageSize,
72-
onlyIfCached: onlyIfCached,
73-
noCacheStore: onlyIfCached,
74-
} satisfies SearchByValueRequest,
75-
{ timeout: 0 },
76-
);
77-
},
59+
queryFn: () => getSearchByValue(params),
7860
});
7961
}
62+
63+
export function getSearchByValue({
64+
instanceClient,
65+
databaseName,
66+
tableName,
67+
searchAttribute,
68+
sort,
69+
pageIndex,
70+
pageSize,
71+
onlyIfCached,
72+
headers,
73+
}: GetSearchByValueParams) {
74+
const customizedSort = sort.attribute.length && !(sort.attribute === searchAttribute && !sort.descending);
75+
return instanceClient.post<Record<string, unknown>[]>(
76+
'/',
77+
{
78+
operation: 'search_by_value',
79+
get_attributes: ['*'],
80+
database: databaseName,
81+
table: tableName,
82+
search_attribute: searchAttribute,
83+
search_value: '*',
84+
sort: customizedSort ? sort : undefined,
85+
offset: pageIndex * pageSize,
86+
limit: pageSize,
87+
onlyIfCached: onlyIfCached,
88+
noCacheStore: onlyIfCached,
89+
} satisfies SearchByValueRequest,
90+
{ timeout: 0, headers },
91+
);
92+
}

0 commit comments

Comments
 (0)