Skip to content

Commit 1a91ad9

Browse files
authored
Merge pull request #17 from ArweaveTeam/feature/select-miner-dropdown
feat: select-miner temporary dropdown until better design
2 parents 976dd5e + 1d630cc commit 1a91ad9

File tree

3 files changed

+157
-9
lines changed

3 files changed

+157
-9
lines changed

renderer/components/add-miner/use-add-miner.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@ export const useAddMiner = () => {
2121
const handleAddMiner = useCallback(() => {
2222
dispatch(appendNode(newMinerData));
2323
setIsModalOpen(false);
24-
}, [dispatch, newMinerData, setIsModalOpen]);
24+
setNewMinerName("");
25+
setNewMinerHost("");
26+
setNewMinerPort(1984);
27+
setNewMinerProtocol("http");
28+
}, [
29+
dispatch,
30+
newMinerData,
31+
setIsModalOpen,
32+
setNewMinerName,
33+
setNewMinerHost,
34+
setNewMinerPort,
35+
setNewMinerProtocol,
36+
]);
2537

2638
const handleNewMinerNameChange = useCallback(
2739
(event: React.ChangeEvent<HTMLInputElement>) => setNewMinerName(event.currentTarget.value),
Lines changed: 135 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,142 @@
1-
import { useSelectedNode } from "../../store/configSlice/configSliceHooks";
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
import { useAppDispatch } from "../../store";
3+
import { useConfigNodes, useSelectedNode } from "../../store/configSlice/configSliceHooks";
4+
import { selectNode } from "../../store/configSlice/configSlice";
5+
import { useAddMiner } from "../add-miner/use-add-miner";
6+
import { AddMinerModal } from "../add-miner/add-miner-modal";
27

38
export function SelectMinerDropdown() {
9+
const dropdownRef = useRef<HTMLDivElement>(null);
10+
const dispatch = useAppDispatch();
11+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
412
const selectedNode = useSelectedNode();
13+
const configuredNodes = useConfigNodes();
14+
const {
15+
isModalOpen,
16+
newMinerName,
17+
newMinerHost,
18+
newMinerPort,
19+
newMinerProtocol,
20+
handleAddMiner,
21+
handleNewMinerNameChange,
22+
handleNewMinerHostChange,
23+
handleNewMinerPortChange,
24+
handleNewMinerProtocolChange,
25+
setIsModalOpen,
26+
} = useAddMiner();
27+
28+
const handleSelectNode = useCallback(
29+
(nodeId: string) => {
30+
if (selectedNode?.id !== nodeId) {
31+
dispatch(selectNode(nodeId));
32+
}
33+
setIsDropdownOpen(false);
34+
},
35+
[dispatch],
36+
);
37+
38+
const handleOutsideClick = useCallback(
39+
(event: MouseEvent) => {
40+
if (
41+
dropdownRef.current &&
42+
event.target &&
43+
!dropdownRef.current.contains(event.target as Node)
44+
) {
45+
setIsDropdownOpen(false);
46+
}
47+
},
48+
[setIsDropdownOpen],
49+
);
50+
51+
useEffect(() => {
52+
if (isDropdownOpen) {
53+
document.addEventListener("mousedown", handleOutsideClick);
54+
} else {
55+
document.removeEventListener("mousedown", handleOutsideClick);
56+
}
57+
58+
// Cleanup the event listener on unmount
59+
return () => {
60+
document.removeEventListener("mousedown", handleOutsideClick);
61+
};
62+
}, [isDropdownOpen]);
563

664
return (
7-
<button
8-
className="flex items-center gap-2 border border-gray-950 rounded-md px-4 py-2 font-normal outline-none text-gray-950 hover:bg-gray-200"
9-
type="button"
10-
>
11-
<span>{selectedNode?.name || "loading..."}</span>
12-
</button>
65+
<div className="relative" ref={dropdownRef}>
66+
<button
67+
className="flex items-center gap-2 border border-gray-950 rounded-md px-4 py-2 font-normal outline-none text-gray-950 hover:bg-gray-200"
68+
type="button"
69+
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
70+
>
71+
<span>{selectedNode?.name || "loading..."}</span>
72+
</button>
73+
{isDropdownOpen && (
74+
<div className="absolute z-10 top-0 right-0 mt-12 w-56 rounded-md shadow-lg bg-gray-950 ring-1 ring-black ring-opacity-5">
75+
<div
76+
className="py-1"
77+
role="menu"
78+
aria-orientation="vertical"
79+
aria-labelledby="options-menu"
80+
>
81+
{configuredNodes.map((node) => (
82+
<a
83+
key={node.id}
84+
href="#"
85+
className="block px-2 py-2 text-sm text-gray-200 hover:bg-gray-700 hover:text-white"
86+
role="menuitem"
87+
onClick={(event) => {
88+
if (event.target instanceof HTMLButtonElement) {
89+
event.preventDefault();
90+
} else {
91+
handleSelectNode(node.id);
92+
}
93+
}}
94+
>
95+
{node.id === selectedNode?.id ? <span></span> : <span className="w-2">·</span>}{" "}
96+
{node.name}{" "}
97+
<button
98+
className="rounded-full p-0 px-2 hover:bg-gray-500 border-gray-200 border-solid border-2"
99+
onClick={() => window.alert("TODO: edit miner")}
100+
>
101+
&#x270E;
102+
</button>
103+
<button
104+
className="rounded-full p-0 px-2 ml-2 hover:bg-gray-500 border-gray-200 border-solid border-2"
105+
onClick={() => window.alert("TODO: delete miner")}
106+
>
107+
&#x1F5D1;
108+
</button>
109+
</a>
110+
))}
111+
<hr />
112+
<a
113+
href="#"
114+
className="block px-4 py-2 text-sm text-gray-200 hover:bg-gray-700 hover:text-white"
115+
role="menuitem"
116+
onClick={() => {
117+
setIsDropdownOpen(false);
118+
setIsModalOpen(true);
119+
}}
120+
>
121+
<span>+</span> Add new miner
122+
</a>
123+
</div>
124+
</div>
125+
)}
126+
{isModalOpen && (
127+
<AddMinerModal
128+
onClose={() => setIsModalOpen(false)}
129+
onAddMiner={handleAddMiner}
130+
nameValue={newMinerName}
131+
hostnameValue={newMinerHost}
132+
portValue={newMinerPort}
133+
protocolValue={newMinerProtocol}
134+
onNameChange={handleNewMinerNameChange}
135+
onHostnameChange={handleNewMinerHostChange}
136+
onPortChange={handleNewMinerPortChange}
137+
onProtocolChange={handleNewMinerProtocolChange}
138+
/>
139+
)}
140+
</div>
13141
);
14142
}

renderer/store/configSlice/configSlice.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export const configSlice = createSlice({
1717
name: "config",
1818
initialState,
1919
reducers: {
20+
selectNode(state, action: PayloadAction<string>) {
21+
state.selectedNode = action.payload;
22+
window.ipc.setSelectedNodeById(action.payload);
23+
},
2024
setNodes(state, action: PayloadAction<ArweaveNodeConfig[]>) {
2125
state.nodes = action.payload;
2226
if (state.selectedNode === undefined && action.payload.length > 0) {
@@ -27,6 +31,10 @@ export const configSlice = createSlice({
2731
appendNode(state, action: PayloadAction<NewArweaveNodeConfig>) {
2832
const newNode = window.ipc.configAppendNode(action.payload);
2933
state.nodes.push(newNode);
34+
if (state.selectedNode === undefined) {
35+
state.selectedNode = newNode.id;
36+
window.ipc.setSelectedNodeById(newNode.id);
37+
}
3038
},
3139
},
3240
});
@@ -36,5 +44,5 @@ export const getNodes = createAsyncThunk("config/getNodes", async (_, { dispatch
3644
dispatch(configSlice.actions.setNodes(answer));
3745
});
3846

39-
export const { appendNode } = configSlice.actions;
47+
export const { appendNode, selectNode } = configSlice.actions;
4048
export default configSlice.reducer;

0 commit comments

Comments
 (0)