Skip to content

Commit 26e9358

Browse files
committed
feat(core): fix elastic search functionality to include document json directly
feat(frontend): add new filter logic to work with both SQL and NoSQL alike!
1 parent 640f2b3 commit 26e9358

File tree

10 files changed

+245
-42
lines changed

10 files changed

+245
-42
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/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
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import classNames from "classnames";
2+
import { AnimatePresence, motion } from "framer-motion";
3+
import { FC, useCallback, useEffect, useMemo, useState } from "react";
4+
import { ActionButton, AnimatedButton } from "../../components/button";
5+
import { createDropdownItem, Dropdown, IDropdownItem } from "../../components/dropdown";
6+
import { Icons } from "../../components/icons";
7+
import { Input, Label } from "../../components/input";
8+
9+
export type IExploreStorageUnitWhereConditionFilter = {
10+
field: string;
11+
operator: string;
12+
value: string;
13+
}
14+
15+
type IExploreStorageUnitWhereConditionProps = {
16+
options: string[];
17+
operators: string[];
18+
onChange?: (filters: IExploreStorageUnitWhereConditionFilter[]) => void;
19+
}
20+
21+
export const ExploreStorageUnitWhereCondition: FC<IExploreStorageUnitWhereConditionProps> = ({ options, onChange, operators }) => {
22+
const [newFilter, setNewFilter] = useState<IExploreStorageUnitWhereConditionFilter>({ field: options[0], operator: operators[0], value: "" });
23+
const [filters, setFitlers] = useState<IExploreStorageUnitWhereConditionFilter[]>([]);
24+
const [show, setShow] = useState(false);
25+
26+
const handleClick = useCallback(() => {
27+
setShow(s => !s);
28+
}, []);
29+
30+
const fieldsDropdownItems = useMemo(() => {
31+
return options.map(option => createDropdownItem(option));
32+
}, [options]);
33+
34+
const handleFieldSelect = useCallback((item: IDropdownItem) => {
35+
setNewFilter(val => ({
36+
...val,
37+
field: item.id,
38+
}));
39+
}, []);
40+
41+
const handleOperatorSelector = useCallback((item: IDropdownItem) => {
42+
setNewFilter(val => ({
43+
...val,
44+
operator: item.id,
45+
}));
46+
}, []);
47+
48+
const handleInputChange = useCallback((newValue: string) => {
49+
setNewFilter(val => ({
50+
...val,
51+
value: newValue,
52+
}));
53+
}, []);
54+
55+
const handleAddFilter = useCallback(() => {
56+
const newFilters = [...filters, newFilter];
57+
setFitlers(newFilters);
58+
setNewFilter({ field: newFilter.field, operator: operators[0], value: "" });
59+
onChange?.(newFilters);
60+
}, [filters, newFilter, onChange, operators]);
61+
62+
const handleRemove = useCallback((index: number) => {
63+
setFitlers(oldFilters => oldFilters.filter((_, i) => i !== index));
64+
}, []);
65+
66+
useEffect(() => {
67+
setNewFilter(f => ({
68+
...f,
69+
operator: operators[0],
70+
}));
71+
}, [operators]);
72+
73+
const validOperators = useMemo(() => {
74+
return operators.map(operator => createDropdownItem(operator));
75+
}, [operators]);
76+
77+
return <div className="flex flex-col gap-1 h-full relative">
78+
<Label label="Where condition" />
79+
<div className="flex gap-1 items-center max-w-[min(500px,calc(100vw-20px))] flex-wrap">
80+
{
81+
filters.map((filter, i) => (
82+
<div className="group/filter-item flex gap-1 items-center text-xs px-2 py-1 rounded-2xl dark:bg-white/5 cursor-pointer relative overflow-hidden shadow-sm border border-neutral-100 dark:border-neutral-800"
83+
onClick={() => handleRemove(i)}>
84+
<div className="max-w-[350px] truncate dark:text-neutral-300">
85+
{filter.field} {filter.operator} {filter.value}
86+
</div>
87+
<ActionButton icon={Icons.Cancel} containerClassName="hover:scale-125 absolute right-0 top-1/2 -translate-y-1/2 z-10 h-4 w-4 opacity-0 group-hover/filter-item:opacity-100" />
88+
</div>
89+
))
90+
}
91+
<ActionButton className={classNames("transition-all", {
92+
"rotate-45": show,
93+
})} icon={Icons.Add} containerClassName="h-8 w-8" onClick={handleClick} />
94+
</div>
95+
<AnimatePresence mode="wait">
96+
{
97+
show &&
98+
<motion.div className="flex gap-1 z-[5] py-2 px-4 absolute top-full mt-1 rounded-lg shadow-md border border-neutral-100 dark:border-white/5 dark:bg-white/20 dark:backdrop-blur-xl translate-y-full bg-white" initial={{
99+
y: -10,
100+
opacity: 0,
101+
}} animate={{
102+
y: 0,
103+
opacity: 1,
104+
}} exit={{
105+
y: -10,
106+
opacity: 0,
107+
}}>
108+
<Dropdown className="min-w-[100px]" value={createDropdownItem(newFilter.field)} items={fieldsDropdownItems} onChange={handleFieldSelect} />
109+
<Dropdown className="min-w-20" value={createDropdownItem(newFilter.operator)} items={validOperators} onChange={handleOperatorSelector} />
110+
<Input inputProps={{
111+
className: "min-w-[150px]"
112+
}} placeholder="Enter filter value" value={newFilter.value} setValue={handleInputChange} />
113+
<AnimatedButton className="dark:bg-white/5" icon={Icons.CheckCircle} label="Add" onClick={handleAddFilter} />
114+
</motion.div>
115+
}
116+
</AnimatePresence>
117+
</div>
118+
}

0 commit comments

Comments
 (0)