feat: Account > Node Settings screen

This commit is contained in:
Ahmed Bouhuolia 2022-05-16 21:20:18 +02:00 committed by GitHub
parent ba4b634aaa
commit bc6ed842d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 556 additions and 39 deletions

View File

@ -39,6 +39,10 @@ export enum LangKeys {
AccountNodeFieldDeamonFlags = "account.nodeSecurity.deamonFlags",
AccountNodeFieldPort = "account.nodeSecurity.port",
AccountNodeStopDeamon = "account.nodeSecurity.stopDeamon",
AccountNodeStartDeamon = "account.nodeSecurity.startDeamon",
AccountNodeLocalSaveNotification = "account.nodeSecurity.saveNotification",
AccountNodeDeamonStoppedNotif = "account.nodeSecurity.stoppedNotification",
AccountNodeDeamonStartedNotif = "account.nodeSecurity.startedNotification",
AccountSettingsAddNode = "account.settings.addNewNode",
AccountSettingsCurrent = "account.settings.current",
AccountSecurityFieldPassword = "account.security.field.password",

View File

@ -43,6 +43,11 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountNodeFieldPort]: "Port",
[LangKeys.AccountNodeFieldDeamonFlags]: "Deamon startup flags",
[LangKeys.AccountNodeStopDeamon]: "Stop deamon",
[LangKeys.AccountNodeStartDeamon]: "Start deamon",
[LangKeys.AccountNodeLocalSaveNotification]:
"Local node settings updated successfully",
[LangKeys.AccountNodeDeamonStoppedNotif]: "Deamon stopped successfully",
[LangKeys.AccountNodeDeamonStartedNotif]: "Deamon started successfully",
[LangKeys.AccountSettingsAddNode]: "Add a new node",
[LangKeys.AccountSettingsCurrent]: "Current",
[LangKeys.AccountSecurityFieldPassword]: "Update account password",

View File

@ -44,6 +44,11 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.AccountNodeFieldPort]: "Puerto",
[LangKeys.AccountNodeFieldDeamonFlags]: "Indicadores de inicio de daemon",
[LangKeys.AccountNodeStopDeamon]: "Detener demonio",
[LangKeys.AccountNodeStartDeamon]: "Comienzo demonio",
[LangKeys.AccountNodeLocalSaveNotification]:
"La configuración del nodo local se actualizó correctamente.",
[LangKeys.AccountNodeDeamonStoppedNotif]: "Daemon se detuvo con éxito",
[LangKeys.AccountNodeDeamonStartedNotif]: "Daemon se inició con éxito",
[LangKeys.AccountSettingsAddNode]: "Agregar un nuevo nodo",
[LangKeys.AccountSettingsCurrent]: "Actual",
[LangKeys.AccountSecurityFieldPassword]: "Clave",

View File

@ -18,6 +18,10 @@ export enum QueryKeys {
HavenoVersion = "Haveno.Version",
Balances = "Haveno.Balances",
PaymentAccounts = "Haveno.PaymentAccounts",
MoneroNodeSettings = "Haveno.MoneroNodeSettings",
MoneroNodeIsRunning = "Haveno.MoneroNodeIsRunning",
MoneroRemoteNodes = "Haveno.MoneroRemoteNodes",
SyncStatus = "Haveno.SyncStatus",
StorageAccountInfo = "Storage.AccountInfo",

View File

@ -0,0 +1,27 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
export function useIsMoneroNodeRunning() {
const client = useHavenoClient();
return useQuery<boolean, Error>(QueryKeys.MoneroNodeIsRunning, () =>
client.isMoneroNodeRunning()
);
}

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { MoneroNodeSettings } from "haveno-ts";
import { useQuery } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function useMoneroNodeSettings() {
const client = useHavenoClient();
return useQuery<MoneroNodeSettings | undefined, Error>(
QueryKeys.MoneroNodeSettings,
async () => {
return client.getMoneroNodeSettings();
}
);
}

View File

@ -0,0 +1,39 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useQuery } from "react-query";
import { QueryKeys } from "@constants/query-keys";
// import { useHavenoClient } from "./useHavenoClient";
interface MoneroRemoteNodes {
title: string;
isActive: boolean;
}
export function useMoneroRemoteNodes() {
// const client = useHavenoClient();
return useQuery<MoneroRemoteNodes[], Error>(
QueryKeys.MoneroRemoteNodes,
async () => {
return Promise.resolve([
{ title: "node.moneroworldcom:18089", isActive: true },
{ title: "node.xmr.pt:18081", isActive: true },
{ title: "node.monero.net:18081", isActive: true },
]);
}
);
}

View File

@ -0,0 +1,46 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMutation, useQueryClient } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { useHavenoClient } from "./useHavenoClient";
interface SetMeneroNodeSettingsVariables {
blockchainPath?: string;
bootstrapUrl?: string;
startupFlags?: Array<string>;
}
export function useSetMoneroNodeSettings() {
const queryClient = useQueryClient();
const client = useHavenoClient();
return useMutation(
async (data: SetMeneroNodeSettingsVariables) => {
const nodeSettings = await client.getMoneroNodeSettings();
data.blockchainPath &&
nodeSettings?.setBlockchainPath(data.blockchainPath);
data.startupFlags && nodeSettings?.setStartupFlagsList(data.startupFlags);
data.bootstrapUrl && nodeSettings?.setBootstrapUrl(data.bootstrapUrl);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeSettings);
},
}
);
}

View File

@ -0,0 +1,34 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import type { MoneroNodeSettings } from "haveno-ts";
import { useMutation, useQueryClient } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function useStartMoneroNode() {
const client = useHavenoClient();
const queryClient = useQueryClient();
return useMutation<void, Error, MoneroNodeSettings>(
(data: MoneroNodeSettings) => client.startMoneroNode(data),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning);
},
}
);
}

View File

@ -0,0 +1,30 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { useMutation, useQueryClient } from "react-query";
import { useHavenoClient } from "./useHavenoClient";
export function useStopMoneroNode() {
const queryClient = useQueryClient();
const client = useHavenoClient();
return useMutation(() => client.stopMoneroNode(), {
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MoneroNodeIsRunning);
},
});
}

View File

@ -14,28 +14,70 @@
// limitations under the License.
// =============================================================================
import { Box, Stack, Grid, createStyles } from "@mantine/core";
import { useForm } from "@mantine/form";
import { Box, Stack, Grid, Group } from "@mantine/core";
import { joiResolver, useForm } from "@mantine/form";
import { FormattedMessage, useIntl } from "react-intl";
import { showNotification } from "@mantine/notifications";
import { Button } from "@atoms/Buttons";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
import { TextInput } from "@atoms/TextInput";
import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings";
import { useSetMoneroNodeSettings } from "@hooks/haveno/useSetMoneroNodeSettings";
import { NodeLocalStopDeamon } from "./NodeLocalStopDeamon";
import type { NodeLocalFormValues } from "./_hooks";
import { useNodeLocalFormValidation } from "./_hooks";
import { transformSettingsRequestToForm } from "./_utils";
export function NodeLocalForm() {
const form = useForm({
const { data: nodeSettings } = useMoneroNodeSettings();
const { mutateAsync: updateNodeSettings } = useSetMoneroNodeSettings();
const intl = useIntl();
const validation = useNodeLocalFormValidation();
const form = useForm<NodeLocalFormValues>({
initialValues: {
blockchainLocation: "",
startupFlags: "",
deamonAddress: "",
port: "",
...(nodeSettings
? transformSettingsRequestToForm(nodeSettings.toObject())
: {}),
},
validate: joiResolver(validation),
});
const handleFormSubmit = (values: NodeLocalFormValues) => {
updateNodeSettings({
blockchainPath: values.blockchainLocation,
startupFlags: values.startupFlags.split(", "),
bootstrapUrl: `${values.deamonAddress}:${values.port}`,
})
.then(() => {
showNotification({
color: "green",
message: intl.formatMessage({
id: LangKeys.AccountNodeLocalSaveNotification,
defaultMessage: "Local node settings updated successfully",
}),
});
})
.catch((err) => {
console.dir(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
});
};
return (
<Box>
<NodeLocalStopDeamon />
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack spacing="lg">
<TextInput
id="blockchainLocation"
@ -48,7 +90,7 @@ export function NodeLocalForm() {
{...form.getInputProps("blockchainLocation")}
/>
<TextInput
id="deamonFlags"
id="startupFlags"
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldDeamonFlags}
@ -67,6 +109,7 @@ export function NodeLocalForm() {
defaultMessage="Deamon Address"
/>
}
required
{...form.getInputProps("deamonAddress")}
/>
</Grid.Col>
@ -79,33 +122,19 @@ export function NodeLocalForm() {
defaultMessage="Port"
/>
}
required
{...form.getInputProps("port")}
/>
</Grid.Col>
</Grid>
<Group position="right" mt="md">
<Button size="md" type="submit">
<FormattedMessage id={LangKeys.Save} defaultMessage="Save" />
</Button>
</Group>
</Stack>
</form>
</Box>
);
}
function NodeLocalStopDeamon() {
const { classes } = useStyles();
return (
<div className={classes.actions}>
<Button flavor="neutral">
<FormattedMessage
id={LangKeys.AccountNodeStopDeamon}
defaultMessage="Stop deamon"
/>
</Button>
</div>
);
}
const useStyles = createStyles((theme) => ({
actions: {
marginBottom: theme.spacing.xl,
},
}));

View File

@ -0,0 +1,112 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { FormattedMessage, useIntl } from "react-intl";
import { createStyles } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { Button } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings";
import { useStopMoneroNode } from "@hooks/haveno/useStopMoneroNode";
import { useIsMoneroNodeRunning } from "@hooks/haveno/useIsMoneroNodeRunning";
import { useStartMoneroNode } from "@hooks/haveno/useStartMoneroNode";
export function NodeLocalStopDeamon() {
const { classes } = useStyles();
const intl = useIntl();
const { mutateAsync: stopMoneroNode } = useStopMoneroNode();
const { data: isMoneroNodeRunning } = useIsMoneroNodeRunning();
const { mutateAsync: startMoneroNode } = useStartMoneroNode();
const { isLoading: isNodeSettingsLoading, data: nodeSettings } =
useMoneroNodeSettings();
// handle the stop button click.
const handleStopBtnClick = () => {
stopMoneroNode()
.then(() => {
showNotification({
color: "green",
message: intl.formatMessage({
id: LangKeys.AccountNodeDeamonStoppedNotif,
defaultMessage: "Deamon stopped successfully",
}),
});
})
.catch((err) => {
console.dir(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
});
};
// Handle the start button click.
const handleStartBtnClick = () => {
if (!nodeSettings) {
return;
}
startMoneroNode(nodeSettings)
.then(() => {
showNotification({
color: "green",
message: intl.formatMessage({
id: LangKeys.AccountNodeDeamonStartedNotif,
defaultMessage: "Deamon started successfully",
}),
});
})
.catch((err) => {
console.dir(err);
showNotification({
color: "red",
message: err.message,
title: "Something went wrong",
});
});
};
return (
<div className={classes.actions}>
{isMoneroNodeRunning ? (
<Button flavor="neutral" onClick={handleStopBtnClick}>
<FormattedMessage
id={LangKeys.AccountNodeStopDeamon}
defaultMessage="Stop deamon"
/>
</Button>
) : (
<Button
flavor="neutral"
onClick={handleStartBtnClick}
disabled={Boolean(isNodeSettingsLoading || !nodeSettings)}
>
<FormattedMessage
id={LangKeys.AccountNodeStopDeamon}
defaultMessage="Start deamon"
/>
</Button>
)}
</div>
);
}
const useStyles = createStyles((theme) => ({
actions: {
marginBottom: theme.spacing.xl,
},
}));

View File

@ -15,23 +15,26 @@
// =============================================================================
import { Stack, createStyles, Group } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { Button } from "@atoms/Buttons";
import { NodeStatus, NodeStatusType } from "@atoms/NodeStatus";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
import { useMoneroRemoteNodes } from "@hooks/haveno/useMoneroRemoteNodes";
export function NodeRemoteStatus() {
const { data: remoteNodes } = useMoneroRemoteNodes();
return (
<Stack>
<NodeStatus
title="node.moneroworldcom:18089"
status={NodeStatusType.Active}
/>
<NodeStatus title="node.xmr.pt:18081" status={NodeStatusType.Inactive} />
<NodeStatus
title="node.monero.net:18081"
status={NodeStatusType.Active}
/>
{remoteNodes?.map((node) => (
<NodeStatus
key={node.title}
title={node.title}
status={
node.isActive ? NodeStatusType.Active : NodeStatusType.Inactive
}
/>
))}
<AddNewNodeButton />
<Group position="right" mt="sm">

View File

@ -0,0 +1,52 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ReactNode } from "react";
import { BodyText } from "@atoms/Typography";
import { useMoneroNodeSettings } from "@hooks/haveno/useMoneroNodeSettings";
import { useIsMoneroNodeRunning } from "@hooks/haveno/useIsMoneroNodeRunning";
import { useMoneroRemoteNodes } from "@hooks/haveno/useMoneroRemoteNodes";
interface NodeSettingsBootProps {
children: ReactNode;
}
export function LocalNodeSettingsBoot({ children }: NodeSettingsBootProps) {
const { isLoading: isNodeSettingsLoading } = useMoneroNodeSettings();
const { isLoading: isMoneroNodeIsLoading } = useIsMoneroNodeRunning();
return isNodeSettingsLoading || isMoneroNodeIsLoading ? (
<BodyText>Loading settings...</BodyText>
) : (
<>{children}</>
);
}
interface RemoteNodeSettingsBootProps {
children: ReactNode;
}
export function RemoteNodeSettingsBoot({
children,
}: RemoteNodeSettingsBootProps) {
const { isLoading: isMoneroRemoteLoading } = useMoneroRemoteNodes();
return isMoneroRemoteLoading ? (
<BodyText>Loading settings...</BodyText>
) : (
<>{children}</>
);
}

View File

@ -22,6 +22,10 @@ import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeLocalForm } from "./NodeLocalForm";
import { NodeRemoteStatus } from "./NodeRemoteStatus";
import {
LocalNodeSettingsBoot,
RemoteNodeSettingsBoot,
} from "./NodeSettingsBoot";
export function NodeSettingsSwitch() {
const { classes } = useStyles();
@ -43,7 +47,9 @@ export function NodeSettingsSwitch() {
}
icon={<ServerIcon width={32} height={62} />}
>
<NodeLocalForm />
<LocalNodeSettingsBoot>
<NodeLocalForm />
</LocalNodeSettingsBoot>
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
@ -56,7 +62,9 @@ export function NodeSettingsSwitch() {
}
icon={<CloudIcon width={58} height={54} />}
>
<NodeRemoteStatus />
<RemoteNodeSettingsBoot>
<NodeRemoteStatus />
</RemoteNodeSettingsBoot>
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
);

View File

@ -0,0 +1,33 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import * as Joi from "joi";
export interface NodeLocalFormValues {
blockchainLocation: string;
startupFlags: string;
deamonAddress: string;
port: string;
}
export function useNodeLocalFormValidation() {
return Joi.object<NodeLocalFormValues>({
blockchainLocation: Joi.string().empty("").uri({ relativeOnly: true }),
startupFlags: Joi.string().empty(""),
deamonAddress: Joi.string().uri({ allowRelative: false }),
port: Joi.number().port(),
});
}

View File

@ -0,0 +1,55 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { MoneroNodeSettings } from "haveno-ts";
import type { NodeLocalFormValues } from "./_hooks";
/**
* Transformes the settings request values to form.
* @param {MoneroNodeSettings.AsObject} nodeSettings
* @returns {NodeLocalFormValues}
*/
export function transformSettingsRequestToForm(
nodeSettings: MoneroNodeSettings.AsObject
): NodeLocalFormValues {
return {
blockchainLocation: nodeSettings?.blockchainPath || "",
startupFlags: nodeSettings?.startupFlagsList.join(", ") || "",
deamonAddress: transfromBootstrapUrl(nodeSettings?.bootstrapUrl || ""),
port: transformPort(nodeSettings?.bootstrapUrl || ""),
};
}
function transformPort(urlAsString: string) {
try {
const url = new URL(urlAsString);
return url.port;
} catch {
return "";
}
}
function transfromBootstrapUrl(urlAsString: string) {
try {
const url = new URL(urlAsString);
// Remove the port from url.
url.port = "";
return url.href;
} catch {
return "";
}
}