Skip to content

Commit e5afbce

Browse files
authored
Merge pull request #53 from clidey/hk/feature/filter
[Support] UI based filters
2 parents 7cbdcdd + 9900dfc commit e5afbce

File tree

13 files changed

+368
-84
lines changed

13 files changed

+368
-84
lines changed

core/src/plugins/elasticsearch/elasticsearch.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,10 @@ func (p *ElasticSearchPlugin) GetRows(config *engine.PluginConfig, database, col
132132

133133
for _, hit := range hits {
134134
hitMap := hit.(map[string]interface{})
135-
source := hitMap["_source"]
135+
source := hitMap["_source"].(map[string]interface{})
136136
id := hitMap["_id"]
137-
document := map[string]interface{}{}
138-
document["_id"] = id
139-
document["source"] = source
140-
jsonBytes, err := json.Marshal(document)
137+
source["_id"] = id
138+
jsonBytes, err := json.Marshal(source)
141139
if err != nil {
142140
return nil, err
143141
}

core/src/plugins/elasticsearch/update.go

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ import (
1010
"github.com/clidey/whodb/core/src/engine"
1111
)
1212

13-
type JsonSourceMap struct {
14-
Id string `json:"_id"`
15-
Source json.RawMessage `json:"source"`
13+
var script = `
14+
for (entry in params.entrySet()) {
15+
ctx._source[entry.getKey()] = entry.getValue();
1616
}
17+
for (key in ctx._source.keySet().toArray()) {
18+
if (!params.containsKey(key)) {
19+
ctx._source.remove(key);
20+
}
21+
}
22+
`
1723

1824
func (p *ElasticSearchPlugin) UpdateStorageUnit(config *engine.PluginConfig, database string, storageUnit string, values map[string]string) (bool, error) {
1925
client, err := DB(config)
@@ -26,34 +32,24 @@ func (p *ElasticSearchPlugin) UpdateStorageUnit(config *engine.PluginConfig, dat
2632
return false, errors.New("missing 'document' key in values map")
2733
}
2834

29-
jsonSourceMap := &JsonSourceMap{}
30-
if err := json.Unmarshal([]byte(documentJSON), jsonSourceMap); err != nil {
31-
return false, errors.New("source is not correctly formatted")
32-
}
33-
3435
var jsonValues map[string]interface{}
35-
if err := json.Unmarshal(jsonSourceMap.Source, &jsonValues); err != nil {
36+
if err := json.Unmarshal([]byte(documentJSON), &jsonValues); err != nil {
3637
return false, err
3738
}
3839

39-
script := `
40-
for (entry in params.entrySet()) {
41-
ctx._source[entry.getKey()] = entry.getValue();
42-
}
43-
for (key in ctx._source.keySet().toArray()) {
44-
if (!params.containsKey(key)) {
45-
ctx._source.remove(key);
46-
}
47-
}
48-
`
49-
params := jsonValues
40+
id, ok := jsonValues["_id"]
41+
if !ok {
42+
return false, errors.New("missing '_id' field in the document")
43+
}
44+
45+
delete(jsonValues, "_id")
5046

5147
var buf bytes.Buffer
5248
if err := json.NewEncoder(&buf).Encode(map[string]interface{}{
5349
"script": map[string]interface{}{
5450
"source": script,
5551
"lang": "painless",
56-
"params": params,
52+
"params": jsonValues,
5753
},
5854
"upsert": jsonValues,
5955
}); err != nil {
@@ -62,7 +58,7 @@ func (p *ElasticSearchPlugin) UpdateStorageUnit(config *engine.PluginConfig, dat
6258

6359
res, err := client.Update(
6460
storageUnit,
65-
jsonSourceMap.Id,
61+
id.(string),
6662
&buf,
6763
client.Update.WithContext(context.Background()),
6864
client.Update.WithRefresh("true"),

core/src/plugins/mongodb/mongodb.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func (p *MongoDBPlugin) GetStorageUnits(config *engine.PluginConfig, database st
7171
}
7272
return storageUnits, nil
7373
}
74+
7475
func (p *MongoDBPlugin) GetRows(config *engine.PluginConfig, database, collection, filter string, pageSize, pageOffset int) (*engine.GetRowsResult, error) {
7576
client, err := DB(config)
7677
if err != nil {

frontend/src/components/breadcrumbs.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { FC, cloneElement } from "react";
33
import { useNavigate } from "react-router-dom";
44
import { IInternalRoute } from "../config/routes";
55
import { Icons } from "./icons";
6+
import { BRAND_COLOR } from "./classes";
7+
import { twMerge } from "tailwind-merge";
68

79
export type IBreadcrumbRoute = Omit<IInternalRoute, "component">;
810

@@ -21,15 +23,15 @@ export const Breadcrumb: FC<IBreadcrumbProps> = ({ routes, active }) => {
2123
<li key={route.name}>
2224
<div className="flex items-center transition-all gap-2 hover:gap-3 group/breadcrumb dark:text-neutral-300">
2325
{i > 0 && Icons.RightChevron}
24-
<div onClick={() => handleNavigate(route.path)} className={classNames("cursor-pointer text-sm font-medium text-gray-700 hover:text-teal-500 flex items-center gap-2 hover:gap-3 transition-all dark:text-neutral-300", {
25-
"text-teal-800 dark:text-teal-500": active === route,
26-
})}>
26+
<div onClick={() => handleNavigate(route.path)} className={twMerge(classNames("cursor-pointer text-sm font-medium text-neutral-800 hover:text-[#ca6f1e] flex items-center gap-2 hover:gap-3 transition-all dark:text-neutral-300", {
27+
[BRAND_COLOR]: active === route,
28+
}))}>
2729
{
2830
i === 0 &&
2931
<div className="inline-flex items-center text-sm font-medium text-gray-700 dark:text-neutral-300">
3032
{cloneElement(Icons.Home, {
31-
className: classNames("w-3 h-3 group-hover/breadcrumb:fill-teal-500", {
32-
"fill-teal-800 dark:fill-teal-500": active === route,
33+
className: classNames("w-3 h-3 group-hover/breadcrumb:fill-[#ca6f1e]", {
34+
"fill-[#ca6f1e] dark:fill-[#ca6f1e]": active === route,
3335
})
3436
})
3537
}

frontend/src/components/classes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export const BASE_CARD_CLASS = "bg-white h-[200px] w-[200px] rounded-3xl shadow-sm border p-4 flex flex-col justify-between dark:bg-white/10 dark:border-white/5";
2-
export const BRAND_COLOR = "text-[#ca6f1e]";
2+
export const BRAND_COLOR = "text-[#ca6f1e] dark:text-[#ca6f1e]";

frontend/src/components/dropdown.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { Icons } from "./icons";
44
import { Label } from "./input";
55
import { Loading } from "./loading";
66

7+
export function createDropdownItem(option: string, icon?: ReactElement): IDropdownItem {
8+
return {
9+
id: option,
10+
label: option,
11+
icon,
12+
};
13+
}
14+
715
export type IDropdownItem<T extends unknown = any> = {
816
id: string;
917
label: string;
@@ -86,7 +94,7 @@ export const Dropdown: FC<IDropdownProps> = (props) => {
8694
}
8795
{
8896
props.items.length === 0 && props.defaultItem == null &&
89-
<li className="flex items-center gap-1 px-2" onClick={props.onDefaultItemClick}>
97+
<li className="flex items-center gap-1 px-2 dark:text-neutral-300" onClick={props.onDefaultItemClick}>
9098
<div>{Icons.SadSmile}</div>
9199
<div>{props.noItemsLabel}</div>
92100
</li>

frontend/src/components/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const Input: FC<InputProps> = ({ value, setValue, type, placeholder, inpu
3131
}, [inputProps]);
3232

3333
return <input type={type} placeholder={placeholder}
34-
value={value} {...inputProps} onChange={handleChange} onKeyDown={handleKeyDown}
34+
{...inputProps} onChange={handleChange} onKeyDown={handleKeyDown} value={value}
3535
className={twMerge(classNames("appearance-none border border-gray-200 rounded-md w-full p-1 text-gray-700 leading-tight focus:outline-none focus:shadow-outline text-sm h-[34px] px-2 dark:text-neutral-300/100 dark:bg-white/10 dark:border-white/20", inputProps.className))} />
3636
}
3737

frontend/src/components/sidebar/sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export const Sidebar: FC = () => {
231231
onError(error) {
232232
notify(`Error signing you in: ${error.message}`, "error")
233233
},
234-
})
234+
});
235235
}, [dispatch, login, navigate, profiles]);
236236

237237
const handleNavigateToLogin = useCallback(() => {

frontend/src/components/table.tsx

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useExportToCSV, useLongPress } from "./hooks";
1212
import { Icons } from "./icons";
1313
import { SearchInput } from "./search";
1414
import { Loading } from "./loading";
15-
import { clone } from "lodash";
15+
import { clone, values } from "lodash";
1616

1717
type IPaginationProps = {
1818
pageCount: number;
@@ -183,7 +183,7 @@ const TData: FC<ITDataProps> = ({ cell, onCellUpdate, disableEdit }) => {
183183
"bg-gray-200 dark:bg-white/10 blur-[2px]": editable || preview,
184184
})}
185185
>
186-
<span className="hidden">{editedData}</span>
186+
<span className="cell-data hidden">{editedData}</span>
187187
<div
188188
className={classNames("w-full h-full p-2 leading-tight focus:outline-none focus:shadow-outline appearance-none transition-all duration-300 border-solid border-gray-200 dark:border-white/5 overflow-hidden whitespace-nowrap select-none text-gray-600 dark:text-neutral-300", {
189189
"group-even/row:bg-gray-100 hover:bg-gray-300 group-even/row:hover:bg-gray-300 dark:group-even/row:bg-white/10 dark:group-odd/row:bg-white/5 dark:group-even/row:hover:bg-white/15 dark:group-odd/row:hover:bg-white/15": !editable,
@@ -305,7 +305,7 @@ const TableRow: FC<ITableRow> = ({ row, style, onRowUpdate, disableEdit }) => {
305305
<div className="table-row-group text-xs group/row" {...props} key={props.key}>
306306
{
307307
row.cells.map((cell) => (
308-
<TData key={cell.getCellProps().key} cell={cell} onCellUpdate={handleCellUpdate} disableEdit={disableEdit} />
308+
<TData key={cell.getCellProps().key} cell={cell} onCellUpdate={handleCellUpdate} disableEdit={disableEdit || cell.column.id === "#"} />
309309
))
310310
}
311311
</div>
@@ -325,6 +325,7 @@ type ITableProps = {
325325
}
326326

327327
export const Table: FC<ITableProps> = ({ className, columns: actualColumns, rows: actualRows, columnTags, totalPages, currentPage, onPageChange, onRowUpdate, disableEdit }) => {
328+
const fixedTableRef = useRef<FixedSizeList>(null);
328329
const containerRef = useRef<HTMLDivElement>(null);
329330
const operationsRef = useRef<HTMLDivElement>(null);
330331
const tableRef = useRef<HTMLTableElement>(null);
@@ -408,51 +409,51 @@ export const Table: FC<ITableProps> = ({ className, columns: actualColumns, rows
408409
}, [rows]);
409410

410411
const handleKeyUp = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
411-
if (tableRef.current == null) {
412+
if (tableRef.current == null || search.length === 0) {
412413
return;
413414
}
414415
let interval: NodeJS.Timeout;
415416
if (e.key === "Enter") {
416-
let newSearchIndex = (searchIndex + 1) % rowCount;
417-
setSearchIndex(newSearchIndex);
418417
const searchText = search.toLowerCase();
419-
let index = 0;
420-
const tbody = tableRef.current.querySelector(".tbody");
421-
if (tbody == null) {
422-
return;
423-
}
424-
for (const childNode of tbody.childNodes) {
425-
if (childNode instanceof HTMLTableRowElement) {
426-
const text = childNode.textContent?.toLowerCase();
418+
const filteredToOriginalIndex = [];
419+
for (const [index, row] of rows.entries()) {
420+
for (const value of values(row.values)) {
421+
const text = value.toLowerCase();
427422
if (text != null && searchText != null && text.includes(searchText)) {
428-
if (index === newSearchIndex) {
429-
childNode.scrollIntoView({
430-
behavior: "smooth",
431-
block: "center",
432-
inline: "center",
433-
});
434-
for (const cell of childNode.querySelectorAll("input")) {
435-
if (cell instanceof HTMLInputElement) {
436-
cell.classList.add("!bg-yellow-100");
437-
interval = setTimeout(() => {
438-
cell.classList.remove("!bg-yellow-100");
439-
}, 3000);
440-
}
423+
filteredToOriginalIndex.push(index);
424+
}
425+
}
426+
}
427+
428+
if (rows.length > 0 && filteredToOriginalIndex.length > 0) {
429+
const newSearchIndex = (searchIndex + 1) % filteredToOriginalIndex.length;
430+
setSearchIndex(newSearchIndex);
431+
const originalIndex = filteredToOriginalIndex[newSearchIndex] + 1;
432+
fixedTableRef.current?.scrollToItem(originalIndex, "center");
433+
setTimeout(() => {
434+
const currentVisibleRows = tableRef.current?.querySelectorAll(".table-row-group") ?? [];
435+
for (const currentVisibleRow of currentVisibleRows) {
436+
const text = currentVisibleRow.querySelector("div > span")?.textContent ?? "";
437+
if (isNumeric(text)) {
438+
const id = parseInt(text);
439+
if (id === originalIndex) {
440+
currentVisibleRow.classList.add("!bg-yellow-100", "dark:!bg-yellow-800");
441+
interval = setTimeout(() => {
442+
currentVisibleRow.classList.remove("!bg-yellow-100", "dark:!bg-yellow-800");
443+
}, 3000);
441444
}
442-
return;
443445
}
444-
index++;
445446
}
446-
}
447-
};
447+
}, 100);
448+
}
448449
}
449450

450451
return () => {
451452
if (interval != null) {
452453
clearInterval(interval);
453454
}
454455
}
455-
}, [search, rowCount, searchIndex]);
456+
}, [rows, search, searchIndex]);
456457

457458
const handleSearchChange = useCallback((newValue: string) => {
458459
setSearchIndex(-1);
@@ -541,6 +542,7 @@ export const Table: FC<ITableProps> = ({ className, columns: actualColumns, rows
541542
</div>
542543
<div className="tbody" {...getTableBodyProps()}>
543544
<FixedSizeList
545+
ref={fixedTableRef}
544546
height={height}
545547
itemCount={sortedRows.length}
546548
itemSize={31}

frontend/src/pages/raw-execute/raw-execute.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ import { Table } from "../../components/table";
1010
import { InternalRoutes } from "../../config/routes";
1111
import { DatabaseType, useRawExecuteLazyQuery } from "../../generated/graphql";
1212
import { useAppSelector } from "../../store/hooks";
13+
import classNames from "classnames";
1314

1415
type IRawExecuteCellProps = {
1516
cellId: string;
1617
onAdd: (cellId: string) => void;
1718
onDelete?: (cellId: string) => void;
19+
showTools?: boolean;
1820
}
1921

20-
const RawExecuteCell: FC<IRawExecuteCellProps> = ({ cellId, onAdd, onDelete }) => {
22+
const RawExecuteCell: FC<IRawExecuteCellProps> = ({ cellId, onAdd, onDelete, showTools }) => {
2123
const [code, setCode] = useState("");
2224
const [rawExecute, { data: rows, loading, error }] = useRawExecuteLazyQuery();
2325

@@ -50,7 +52,9 @@ const RawExecuteCell: FC<IRawExecuteCellProps> = ({ cellId, onAdd, onDelete }) =
5052
: <CodeEditor language="sql" value={code} setValue={setCode} onRun={handleRawExecute} />
5153
}
5254
</div>
53-
<div className="absolute -bottom-3 z-20 flex justify-between px-3 pr-8 w-full opacity-0 transition-all duration-500 group-hover/cell:opacity-100">
55+
<div className={classNames("absolute -bottom-3 z-20 flex justify-between px-3 pr-8 w-full opacity-0 transition-all duration-500 group-hover/cell:opacity-100", {
56+
"opacity-100": showTools,
57+
})}>
5458
<div className="flex gap-2">
5559
<AnimatedButton icon={Icons.PlusCircle} label="Add" onClick={handleAdd} />
5660
{
@@ -102,7 +106,8 @@ export const RawExecutePage: FC = () => {
102106
cellIds.map((cellId, index) => (
103107
<>
104108
{index > 0 && <div className="border-dashed border-t border-gray-300 my-2 dark:border-neutral-600"></div>}
105-
<RawExecuteCell key={cellId} cellId={cellId} onAdd={handleAdd} onDelete={cellIds.length <= 1 ? undefined : handleDelete} />
109+
<RawExecuteCell key={cellId} cellId={cellId} onAdd={handleAdd} onDelete={cellIds.length <= 1 ? undefined : handleDelete}
110+
showTools={cellIds.length === 1} />
106111
</>
107112
))
108113
}

0 commit comments

Comments
 (0)