Add connection window

This commit is contained in:
Jake Walker 2022-08-10 14:24:58 +01:00
parent deb55c2c6d
commit 04396259a0
11 changed files with 138 additions and 13 deletions

View file

@ -1,2 +1,3 @@
allowedServers: allowedServers:
- "195.201.123.169:16000"
- "127.0.0.1:16000" - "127.0.0.1:16000"

View file

@ -3,7 +3,7 @@ module git.vh7.uk/jakew/echo-web/bridge
go 1.18 go 1.18
require ( require (
git.vh7.uk/jakew/echo-go v0.0.0-20220805101505-158189ae7d58 git.vh7.uk/jakew/echo-go v0.0.0-20220809112338-5bd7802455cb
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/rs/zerolog v1.27.0 github.com/rs/zerolog v1.27.0
github.com/samber/lo v1.27.0 github.com/samber/lo v1.27.0
@ -17,3 +17,5 @@ require (
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
) )
replace git.vh7.uk/jakew/echo-go => ../../echo-go

View file

@ -2,6 +2,8 @@ git.vh7.uk/jakew/echo-go v0.0.0-20220803185705-357282c0e444 h1:/GiYRoIHnYqoF9bAH
git.vh7.uk/jakew/echo-go v0.0.0-20220803185705-357282c0e444/go.mod h1:Q4YqOodoX+qYvfM0gSf78cCk0o7dkg1GLNYUFgB6qhU= git.vh7.uk/jakew/echo-go v0.0.0-20220803185705-357282c0e444/go.mod h1:Q4YqOodoX+qYvfM0gSf78cCk0o7dkg1GLNYUFgB6qhU=
git.vh7.uk/jakew/echo-go v0.0.0-20220805101505-158189ae7d58 h1:gMa91PLcG4jRunbntD/YSt/7U3eSyAHUNxIdWu4ELjU= git.vh7.uk/jakew/echo-go v0.0.0-20220805101505-158189ae7d58 h1:gMa91PLcG4jRunbntD/YSt/7U3eSyAHUNxIdWu4ELjU=
git.vh7.uk/jakew/echo-go v0.0.0-20220805101505-158189ae7d58/go.mod h1:Q4YqOodoX+qYvfM0gSf78cCk0o7dkg1GLNYUFgB6qhU= git.vh7.uk/jakew/echo-go v0.0.0-20220805101505-158189ae7d58/go.mod h1:Q4YqOodoX+qYvfM0gSf78cCk0o7dkg1GLNYUFgB6qhU=
git.vh7.uk/jakew/echo-go v0.0.0-20220809112338-5bd7802455cb h1:wE2bgtd4F/dJKVmMkxqHjQs16vsrqo4TuNbLIM4QX28=
git.vh7.uk/jakew/echo-go v0.0.0-20220809112338-5bd7802455cb/go.mod h1:Q4YqOodoX+qYvfM0gSf78cCk0o7dkg1GLNYUFgB6qhU=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

View file

@ -15,7 +15,7 @@ import (
) )
type Config struct { type Config struct {
AllowedServers []string `yaml:"allowedServers"` AllowedServers []string `yaml:"allowedServers" json:"allowedServers"`
} }
type websocketHandler struct { type websocketHandler struct {
@ -110,6 +110,25 @@ func (h *websocketHandler) socketHandler(w http.ResponseWriter, r *http.Request)
} }
} }
func (h *websocketHandler) configHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
bytes, err := json.Marshal(h.config)
if err != nil {
log.Error().Err(err).Msg("failed to marshal config")
w.WriteHeader(500)
return
}
_, err = w.Write(bytes)
if err != nil {
log.Error().Err(err).Msg("failed to write bytes to response")
w.WriteHeader(500)
return
}
}
func main() { func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
@ -130,6 +149,7 @@ func main() {
} }
http.HandleFunc("/", handler.socketHandler) http.HandleFunc("/", handler.socketHandler)
http.HandleFunc("/config", handler.configHandler)
log.Info().Msg("starting server") log.Info().Msg("starting server")
err = http.ListenAndServe(":4000", nil) err = http.ListenAndServe(":4000", nil)

View file

@ -2,9 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Echo Web</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,9 +1,11 @@
import { Box, Button, Navbar, NavLink, Stack } from "@mantine/core"; import { Box, Button, Navbar, NavLink, Stack } from "@mantine/core";
import { openModal } from "@mantine/modals";
import { Root } from "react-dom/client"; import { Root } from "react-dom/client";
import { Hash, Settings } from "react-feather"; import { Hash, Settings } from "react-feather";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { chatActions } from "../slices/chat"; import { chatActions } from "../slices/chat";
import { RootState } from "../store"; import { RootState } from "../store";
import ConnectModal from "./ConnectModal";
function ChannelList(props: { hidden: boolean }) { function ChannelList(props: { hidden: boolean }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -44,7 +46,7 @@ function ChannelList(props: { hidden: boolean }) {
> >
{connectText} {connectText}
</Button> </Button>
<Button variant="outline"> <Button variant="outline" onClick={() => openModal({ title: "Settings", children: <ConnectModal /> })} >
<Settings size={20} /> <Settings size={20} />
</Button> </Button>
</Button.Group> </Button.Group>

View file

@ -0,0 +1,63 @@
import { Button, Select, SelectItem, Text, TextInput } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
import { Check, CheckCircle, X, XCircle } from "react-feather";
import { useDispatch, useSelector } from "react-redux";
import { chatActions } from "../slices/chat";
import { RootState } from "../store";
function ConnectModal() {
const dispatch = useDispatch();
const stateBridgeAddress = useSelector((state: RootState) => state.chat.bridgeAddress);
const stateServerAddress = useSelector((state: RootState) => state.chat.serverAddress);
const stateUsername = useSelector((state: RootState) => state.chat.username);
const statePassword = useSelector((state: RootState) => state.chat.password);
const [bridgeAddress, setBridgeAddress] = useState<string>(stateBridgeAddress);
const [serverAddress, setServerAddress] = useState<string>(stateServerAddress || "");
const [credentials, setCredentials] = useState<{
username: string,
password: string
}>({
username: stateUsername,
password: statePassword || ""
});
const saveConnectionDetails = () => {
if (bridgeAddress.trim() === "" || serverAddress.trim() === "" || credentials.username.trim() === "") {
showNotification({
title: "Invalid Connection Parameters",
message: "One of the required connection parameters is empty or not valid.",
color: "red",
icon: <X />
});
return;
}
dispatch(chatActions.setConnectionParameters({
bridgeAddress,
serverAddress,
...credentials
}));
showNotification({
title: "Connecton Parameters Saved",
message: `Connecting to ${serverAddress} via ${bridgeAddress} as ${credentials.username}.`,
color: "green",
icon: <Check />
})
}
return (
<>
<Text size="md" mb="sm">Connection Settings</Text>
<TextInput label="Bridge URL" required value={bridgeAddress} onChange={(event) => setBridgeAddress(event.target.value)} />
<TextInput label="Server" required value={serverAddress} onChange={(event) => setServerAddress(event.target.value)} />
<TextInput label="Username" required value={credentials.username} onChange={(event) => setCredentials({ ...credentials, username: event.target.value })} />
<TextInput label="Password" value={credentials.password} onChange={(event) => setCredentials({ ...credentials, password: event.target.value })} />
<Button mt="sm" sx={{ float: "right" }} onClick={saveConnectionDetails}>Save</Button>
</>
);
}
export default ConnectModal;

View file

@ -5,13 +5,19 @@ import App from './App'
import './index.css'; import './index.css';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from './store'; import { store } from './store';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<MantineProvider withGlobalStyles withNormalizeCSS> <Provider store={store}>
<Provider store={store}> <MantineProvider withGlobalStyles withNormalizeCSS>
<App /> <NotificationsProvider>
</Provider> <ModalsProvider>
</MantineProvider> <App />
</ModalsProvider>
</NotificationsProvider>
</MantineProvider>
</Provider>
</React.StrictMode> </React.StrictMode>
) )

View file

@ -1,5 +1,6 @@
import { Middleware } from "@reduxjs/toolkit"; import { Middleware } from "@reduxjs/toolkit";
import chat, { chatActions } from "../slices/chat"; import chat, { chatActions } from "../slices/chat";
import { RootState } from "../store";
enum MessageType { enum MessageType {
ReqServerInfo = "serverInfoRequest", ReqServerInfo = "serverInfoRequest",
@ -48,7 +49,17 @@ const chatMiddleware: Middleware = (store) => {
return (next) => (action) => { return (next) => (action) => {
if (chatActions.connect.match(action) && socket === null) { if (chatActions.connect.match(action) && socket === null) {
socket = new WebSocket("ws://127.0.0.1:4000?server=127.0.0.1:16000&username=jake&password=password"); const nextState: RootState = store.getState();
let connectionString = `${nextState.chat.bridgeAddress}?server=${nextState.chat.serverAddress}&username=${nextState.chat.username}`;
connectionString = connectionString.replace("http://", "ws://").replace("https://", "wss://");
if (nextState.chat.password && nextState.chat.password !== "") {
connectionString += `&password=${nextState.chat.password}`;
}
console.log(`Connecting to ${connectionString}...`);
socket = new WebSocket(connectionString);
socket.onopen = () => { socket.onopen = () => {
store.dispatch(chatActions.connected()); store.dispatch(chatActions.connected());

View file

@ -2,6 +2,11 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ChatMessage } from "../middleware/chat"; import { ChatMessage } from "../middleware/chat";
export interface ChatState { export interface ChatState {
bridgeAddress: string;
serverAddress: string;
username: string;
password?: string;
messages: ChatMessage[]; messages: ChatMessage[];
users: string[]; users: string[];
channels: string[]; channels: string[];
@ -12,6 +17,10 @@ export interface ChatState {
} }
const initialState: ChatState = { const initialState: ChatState = {
bridgeAddress: "http://127.0.0.1:4000",
serverAddress: "127.0.0.1:16000",
username: "echoweb",
messages: [], messages: [],
users: [], users: [],
channels: [], channels: [],
@ -87,7 +96,18 @@ const chatSlice = createSlice({
}, },
sendMessage: (state, action: PayloadAction<{ sendMessage: (state, action: PayloadAction<{
content: string content: string
}>) => {} }>) => {},
setConnectionParameters: (state, action: PayloadAction<{
bridgeAddress: string,
serverAddress: string,
username: string,
password?: string
}>) => {
state.bridgeAddress = action.payload.bridgeAddress;
state.serverAddress = action.payload.serverAddress;
state.username = action.payload.username;
state.password = action.payload.password;
}
} }
}); });