add peers through ui

This commit is contained in:
2024-07-06 18:14:07 +02:00
parent e70649bfc5
commit 9ed53b72eb
18 changed files with 174 additions and 25 deletions

View File

@@ -196,6 +196,7 @@ public class RemoteHostManager {
public Collection<AvailablePeerInfo> getSeenButNotAddedHosts() {
return _seenHostsButNotAdded.entrySet().stream()
.filter(e -> !persistentRemoteHostsService.existsHost(e.getKey()))
.map(e -> new AvailablePeerInfo(e.getKey().toString(), e.getValue().getAddr(), e.getValue().getPort()))
.toList();
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.dhfs.storage.objects.repository.distributed.webapi;
public record KnownPeerInfo(String uuid) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.dhfs.storage.objects.repository.distributed.webapi;
public record KnownPeerPut(String uuid) {
}

View File

@@ -1,6 +1,5 @@
package com.usatiuk.dhfs.storage.objects.repository.distributed.webapi;
import com.usatiuk.dhfs.storage.objects.repository.distributed.HostInfo;
import com.usatiuk.dhfs.storage.objects.repository.distributed.PersistentRemoteHostsService;
import com.usatiuk.dhfs.storage.objects.repository.distributed.RemoteHostManager;
import jakarta.inject.Inject;
@@ -22,14 +21,14 @@ public class ManagementApi {
@Path("known-peers")
@GET
public List<HostInfo> knownPeers() {
return persistentRemoteHostsService.getHosts();
public List<KnownPeerInfo> knownPeers() {
return persistentRemoteHostsService.getHosts().stream().map(h -> new KnownPeerInfo(h.getUuid().toString())).toList();
}
@Path("known-peers")
@PUT
public void addPeer(String hostname) {
remoteHostManager.addRemoteHost(UUID.fromString(hostname));
public void addPeer(KnownPeerPut knownPeerPut) {
remoteHostManager.addRemoteHost(UUID.fromString(knownPeerPut.uuid()));
}
@Path("available-peers")

View File

@@ -8,7 +8,7 @@ import {
import "./App.scss";
import { Home } from "./Home";
import { PeerState } from "./PeerState";
import { peerStateLoader } from "./PeerStatePlumbing";
import { peerStateAction, peerStateLoader } from "./PeerStatePlumbing";
import { ErrorGate } from "./ErrorGate";
const router = createBrowserRouter(
@@ -33,6 +33,7 @@ const router = createBrowserRouter(
path: "peers",
element: <PeerState />,
loader: peerStateLoader,
action: peerStateAction,
},
],
errorElement: <ErrorGate />,

View File

@@ -0,0 +1,13 @@
@import "./PeerCardCommon";
.peerAvailableCard {
@extend .peerCard;
display: flex;
flex-direction: row;
.peerInfo {
flex-grow: 1;
}
}

View File

@@ -0,0 +1,26 @@
import { TAvailablePeerInfoTo } from "./api/dto";
import { useFetcher } from "react-router-dom";
import "./PeerAvailableCard.scss";
export interface TPeerAvailableCardProps {
peerInfo: TAvailablePeerInfoTo;
}
export function PeerAvailableCard({ peerInfo }: TPeerAvailableCardProps) {
const fetcher = useFetcher();
return (
<div className="peerAvailableCard">
<div className={"peerInfo"}>
<span>UUID: </span>
<span>{peerInfo.uuid}</span>
</div>
<fetcher.Form method="put" action={"/home/peers"}>
<input name="intent" hidden={true} value={"add_peer"} />
<input name="uuid" hidden={true} value={peerInfo.uuid} />
<button type="submit">connect</button>
</fetcher.Form>
</div>
);
}

View File

@@ -0,0 +1,7 @@
@import "./common";
.peerCard {
@include border-shadow;
padding: 0.25rem;
}

View File

@@ -0,0 +1,5 @@
@import "./PeerCardCommon";
.peerKnownCard {
@extend .peerCard;
}

View File

@@ -0,0 +1,11 @@
import { TKnownPeerInfoTo } from "./api/dto";
import "./PeerKnownCard.scss";
export interface TPeerKnownCardProps {
peerInfo: TKnownPeerInfoTo;
}
export function PeerKnownCard({ peerInfo }: TPeerKnownCardProps) {
return <div className="peerKnownCard">{peerInfo.uuid}</div>;
}

View File

@@ -2,4 +2,11 @@
#PeerState {
@include home-view;
display: flex;
flex-direction: column;
> * {
margin-top: 1rem;
}
}

View File

@@ -2,13 +2,30 @@ import "./PeerState.scss";
import { useLoaderData } from "react-router-dom";
import { LoaderToType } from "./commonPlumbing";
import { peerStateLoader } from "./PeerStatePlumbing";
import { PeerAvailableCard } from "./PeerAvailableCard";
import { PeerKnownCard } from "./PeerKnownCard";
export function PeerState() {
const loaderData = useLoaderData() as LoaderToType<typeof peerStateLoader>;
const availablePeers = loaderData?.availablePeers.map((p) => {
return <div>{p.uuid}</div>;
});
const knownPeers = loaderData.knownPeers.map((p) => (
<PeerKnownCard peerInfo={p} key={p.uuid} />
));
return <div id={"PeerState"}>{availablePeers}</div>;
const availablePeers = loaderData.availablePeers.map((p) => (
<PeerAvailableCard peerInfo={p} key={p.uuid} />
));
return (
<div id={"PeerState"}>
<div>
<div>Known peers</div>
<div>{knownPeers}</div>
</div>
<div>
<div>Available peers</div>
<div>{availablePeers}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import {
getAvailablePeers,
getKnownPeers,
putKnownPeer,
} from "./api/PeerState";
import { ActionFunctionArgs } from "react-router-dom";
export async function peerStateLoader() {
return {
availablePeers: await getAvailablePeers(),
knownPeers: await getKnownPeers(),
};
}
export type PeerStateActionType = "add_peer" | unknown;
export async function peerStateAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent") as PeerStateActionType;
if (intent === "add_peer") {
return await putKnownPeer(formData.get("uuid") as string);
} else {
throw new Error("Malformed action: " + JSON.stringify(request));
}
}

View File

@@ -1,7 +0,0 @@
import { getAvailablePeers } from "./api/PeerState";
export async function peerStateLoader() {
return {
availablePeers: await getAvailablePeers(),
};
}

View File

@@ -1,8 +1,13 @@
import { fetchJSON_throws } from "./utils";
import { fetchJSON, fetchJSON_throws } from "./utils";
import {
AvailablePeerInfoToResp,
KnownPeerInfoToResp,
NoContentToResp,
TAvailablePeerInfoArrTo,
TAvailablePeerInfoToResp,
TKnownPeerInfoArrTo,
TKnownPeerInfoToResp,
TNoContentToResp,
} from "./dto";
export async function getAvailablePeers(): Promise<TAvailablePeerInfoArrTo> {
@@ -11,3 +16,17 @@ export async function getAvailablePeers(): Promise<TAvailablePeerInfoArrTo> {
typeof AvailablePeerInfoToResp
>("/objects-manage/available-peers", "GET", AvailablePeerInfoToResp);
}
export async function getKnownPeers(): Promise<TKnownPeerInfoArrTo> {
return fetchJSON_throws<TKnownPeerInfoToResp, typeof KnownPeerInfoToResp>(
"/objects-manage/known-peers",
"GET",
KnownPeerInfoToResp,
);
}
export async function putKnownPeer(uuid: string): Promise<TNoContentToResp> {
return fetchJSON("/objects-manage/known-peers", "PUT", NoContentToResp, {
uuid,
});
}

View File

@@ -20,12 +20,14 @@ export type TNoContentTo = z.infer<typeof NoContentTo>;
export const NoContentToResp = CreateAPIResponse(NoContentTo);
export type TNoContentToResp = z.infer<typeof NoContentToResp>;
// TokenRequest
export const TokenRequestTo = z.object({
username: z.string(),
password: z.string(),
});
export type TTokenRequestTo = z.infer<typeof TokenRequestTo>;
// Token
export const TokenTo = z.object({
token: z.string(),
});
@@ -34,6 +36,7 @@ export type TTokenTo = z.infer<typeof TokenTo>;
export const TokenToResp = CreateAPIResponse(TokenTo);
export type TTokenToResp = z.infer<typeof TokenToResp>;
// AvailablePeerInfo
export const AvailablePeerInfoTo = z.object({
uuid: z.string(),
addr: z.string(),
@@ -48,3 +51,19 @@ export const AvailablePeerInfoToResp = CreateAPIResponse(
AvailablePeerInfoArrTo,
);
export type TAvailablePeerInfoToResp = z.infer<typeof AvailablePeerInfoToResp>;
// KnownPeerInfo
export const KnownPeerInfoTo = z.object({
uuid: z.string(),
});
export type TKnownPeerInfoTo = z.infer<typeof KnownPeerInfoTo>;
export const KnownPeerInfoArrTo = z.array(KnownPeerInfoTo);
export type TKnownPeerInfoArrTo = z.infer<typeof KnownPeerInfoArrTo>;
export const KnownPeerInfoToResp = CreateAPIResponse(KnownPeerInfoArrTo);
export type TKnownPeerInfoToResp = z.infer<typeof KnownPeerInfoToResp>;
// KnownPeerPut
export const KnownPeerPutTo = z.object({ uuid: z.string() });
export type TKnownPeerPutTo = z.infer<typeof KnownPeerPutTo>;

View File

@@ -7,7 +7,7 @@ declare const process: {
};
};
const apiRoot: string =
export const apiRoot: string =
process.env.NODE_ENV == "production"
? ""
: "http://localhost:1234/apiproxy";
@@ -91,9 +91,7 @@ export async function fetchJSONAuth<T, P extends { parse: (arg: string) => T }>(
}
}
function errorCheck<A extends unknown[], R>(
fn: (...args: A) => R,
): (...args: A) => Promise<Exclude<Awaited<R>, TErrorTo>> {
function errorCheck<A extends unknown[], R>(fn: (...args: A) => R) {
return async (...args: A): Promise<Exclude<Awaited<R>, TErrorTo>> => {
const ret = await fn(...args);
if (isError(ret)) {

View File

@@ -1,3 +1,3 @@
export type LoaderToType<T extends (...args: any) => any> =
| Exclude<Awaited<ReturnType<T>>, Response>
| undefined;
export type LoaderToType<T extends (...args: unknown[]) => unknown> = Awaited<
ReturnType<T>
>;