Skip to content

Commit

Permalink
setup flow improvements - UI
Browse files Browse the repository at this point in the history
  • Loading branch information
wardviaene committed Sep 11, 2024
1 parent 9743534 commit cb04b34
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 35 deletions.
2 changes: 1 addition & 1 deletion latest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.1.3
v1.1.4
2 changes: 1 addition & 1 deletion pkg/rest/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (c *Context) contextHandler(w http.ResponseWriter, r *http.Request) {
}
}

out, err := json.Marshal(ContextSetupResponse{SetupCompleted: c.SetupCompleted})
out, err := json.Marshal(ContextSetupResponse{SetupCompleted: c.SetupCompleted, CloudType: c.CloudType})
if err != nil {
c.returnError(w, err, http.StatusBadRequest)
return
Expand Down
3 changes: 2 additions & 1 deletion pkg/rest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ type ContextRequest struct {
Protocol string `json:"protocol"`
}
type ContextSetupResponse struct {
SetupCompleted bool `json:"setupCompleted"`
SetupCompleted bool `json:"setupCompleted"`
CloudType string `json:"cloudType"`
}

type AuthMethodsResponse struct {
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/AppInit/AppInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { AppSettings } from '../Constants/Constants';
}

if (!setupCompleted) {
return <SetupBanner onCompleted={setSetupCompleted} />
return <SetupBanner onCompleted={setSetupCompleted} cloudType={data.cloudType} />
} else {
return children
}
Expand Down
19 changes: 16 additions & 3 deletions webapp/src/AppInit/SetAdminPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export function SetAdminPassword({onChangeStep, secret}: Props) {
}
passwordMutation.mutate(password)
}
const captureEnter = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
if(password !== "" && password2 !== "") {
changePassword()
}
}
}
return (
<div className={classes.wrapper}>
<div className={classes.body}>
Expand All @@ -51,26 +58,32 @@ export function SetAdminPassword({onChangeStep, secret}: Props) {
Set a password for the admin user. At the next screen you'll be able to login with the username "admin" and the password you'll set now.
</Text>
{passwordMutation.isPending ? (
<div>Setting Password...</div>
<div>Setting Password for user 'admin'...</div>
) : (
<div>
<Text component="label" htmlFor="your-password" size="sm" fw={500}>
Your password
</Text>
<PasswordInput placeholder="Your password" id="your-password-1"
<PasswordInput
placeholder="Your password for user admin"
id="your-password-1"
autoComplete="new-password"
onChange={(event) => setPassword(event.currentTarget.value)}
value={password}
error={passwordError}
onKeyDown={(e) => captureEnter(e)}
/>
<Text component="label" htmlFor="your-password" size="sm" fw={500}>
Repeat password
</Text>
<PasswordInput
placeholder="Repeat your password"
id="your-password-2"
autoComplete="new-password"
onChange={(event) => setPassword2(event.currentTarget.value)}
value={password2}
error={password2Error}
error={password2Error}
onKeyDown={(e) => captureEnter(e)}
/>
<br />
<Button onClick={() => changePassword()}>Set Admin Password</Button>
Expand Down
152 changes: 130 additions & 22 deletions webapp/src/AppInit/SetSecret.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,97 @@
import { Text, Title, TextInput, Button } from '@mantine/core';
import { Text, Title, TextInput, Button, Card, Grid, Container, Center, Alert, ActionIcon } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { TbCheck, TbCopy } from 'react-icons/tb';
import classes from './SetupBanner.module.css';
import {useState} from 'react';
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { AppSettings } from '../Constants/Constants';
import {
useQueryClient,
useMutation,
} from '@tanstack/react-query'
import { TbInfoCircle } from 'react-icons/tb';

type Props = {
onChangeStep: (newType: number) => void;
onChangeSecret: (newType: string) => void;
};
cloudType: string;
};

export function SetSecret({onChangeStep, onChangeSecret}: Props) {
type SetupResponse = {
secret: string;
tagHash: string;
instanceID: string;
}
type SetupResponseError = {
error: string;
}

const randomHex = (length:number) => {
const bytes = window.crypto.getRandomValues(new Uint8Array(length))
var hexstring='', h;
for(var i=0; i<bytes.length; i++) {
h=bytes[i].toString(16);
if(h.length==1) { h='0'+h; }
hexstring+=h;
}
return hexstring;
}


export function SetSecret({onChangeStep, onChangeSecret, cloudType}: Props) {
const clipboard = useClipboard({ timeout: 120000 });
const queryClient = useQueryClient()
const [secret, setSecret] = useState<string>("");
const [setupResponse, setSetupResponse] = useState<SetupResponse>({secret: "", tagHash: "", instanceID: ""});
const [secretError, setSecretError] = useState<string>("");
const [randomHexValue] = useState(randomHex(16))
const secretMutation = useMutation({
mutationFn: (newSecret: string) => {
mutationFn: (setupResponse: SetupResponse) => {
setSecretError("")
return axios.post(AppSettings.url + '/context', {secret: newSecret})
return axios.post(AppSettings.url + '/context', setupResponse)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['context'] })
onChangeSecret(secret)
onChangeSecret(setupResponse.secret)
onChangeStep(1)
},
onError: (error) => {
if(error.message.includes("status code 403")) {
setSecretError("Invalid secret")
} else {
onError: (error:AxiosError) => {
const errorMessage = error.response?.data as SetupResponseError
if(errorMessage?.error === undefined) {
setSecretError("Error: "+ error.message)
} else {
setSecretError(errorMessage.error)
}
}
},
})
const captureEnter = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
secretMutation.mutate(setupResponse)
}
}
const alertIcon = <TbInfoCircle />
const hasMoreOptions = cloudType === "aws" || cloudType === "digitalocean" ? true : false
const colSpanWithSSH = hasMoreOptions ? 3 : 6

return (
<div className={classes.wrapper}>
<div className={classes.body}>
<Title className={classes.title}>Start Setup...</Title>
<Container fluid style={{marginTop: 50}}>
<Center>
<Title order={1} style={{marginBottom: 20}}>Start Setup</Title>
</Center>
{secretError !== "" ?
<Grid>
<Grid.Col span={3}></Grid.Col>
<Grid.Col span={6}>
<Alert variant="light" color="red" title="Error" radius="lg" icon={alertIcon} className={classes.error} style={{marginBottom: 20, paddingLeft: 20, paddingRight:35}}>{secretError}</Alert>
</Grid.Col>
</Grid>
:
null
}
<Grid>
<Grid.Col span={3}></Grid.Col>
<Grid.Col span={colSpanWithSSH}>
<Card withBorder radius="md" p="xl" className={classes.card}>
<Title order={3} style={{marginBottom: 20}}>{hasMoreOptions ? "Option 1: " : ""}With SSH Access</Title>
<Text fw={500} fz="lg" mb={5}>
Enter the secret to start the setup.
</Text>
Expand All @@ -57,14 +110,69 @@ export function SetSecret({onChangeStep, onChangeSecret}: Props) {
<TextInput
placeholder="secret"
classNames={{ input: classes.input, root: classes.inputWrapper }}
onChange={(event) => setSecret(event.currentTarget.value)}
value={secret}
error={secretError}
onChange={(event) => setSetupResponse({ ...setupResponse, secret: event.currentTarget.value})}
value={setupResponse.secret}
onKeyDown={(e) => captureEnter(e)}
/>
<Button className={classes.control} onClick={() => secretMutation.mutate(secret)}>Continue</Button>
<Button className={classes.control} onClick={() => secretMutation.mutate({ secret: setupResponse.secret, tagHash: "", instanceID: ""})}>Continue</Button>
</div>
)}
</div>
</div>
</Card>
</Grid.Col>
{cloudType === "aws" ?
<Grid.Col span={3}>
<Card withBorder radius="md" p="xl" className={classes.card}>
<Title order={3} style={{marginBottom: 20}}>{hasMoreOptions ? "Option 2: " : ""}Without SSH Access</Title>

<Text>
Enter the EC2 Instance ID of the VPN Server
</Text>
{secretMutation.isPending ? (
<div>Checking Instance ID...</div>
) : (
<div className={classes.controls}>
<TextInput
placeholder="i-1234567890abcdef0"
classNames={{ input: classes.input, root: classes.inputWrapper }}
onChange={(event) => setSetupResponse({ ...setupResponse, instanceID: event.currentTarget.value})}
value={setupResponse.instanceID}
onKeyDown={(e) => captureEnter(e)}
/>
<Button className={classes.control} onClick={() => secretMutation.mutate({ secret: "", tagHash: "", instanceID: setupResponse.instanceID})}>Check Instance ID</Button>
</div>
)}
</Card>
</Grid.Col>
: null }
{cloudType === "digitalocean" ?
<Grid.Col span={3}>
<Card withBorder radius="md" p="xl" className={classes.card}>
<Title order={3} style={{marginBottom: 20}}>{hasMoreOptions ? "Option 2: " : ""}Without SSH Access</Title>

<Text>
Add the following tag to the droplet by going to the <Text span fw={700}>droplet settings</Text> and opening the <Text span fw={700}>Tags</Text> page.
</Text>
{secretMutation.isPending ? (
<div>Checking tag...</div>
) : (
<div className={classes.controls}>
<TextInput
disabled
classNames={{ input: classes.input, root: classes.inputWrapper }}
value={randomHexValue}
leftSection={
<ActionIcon size={32} radius="xl" variant="transparent" onClick={() => clipboard.copy(randomHexValue)}>
{ clipboard.copied ? <TbCheck /> : <TbCopy /> }
</ActionIcon>
}
/>
<Button className={classes.control} onClick={() => secretMutation.mutate({ secret: "", tagHash: randomHexValue, instanceID: ""})}>Check tag</Button>
</div>
)}
</Card>
</Grid.Col>
: null }
</Grid>
</Container>
);
}
8 changes: 4 additions & 4 deletions webapp/src/AppInit/SetupBanner.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
display: flex;
align-items: center;
padding: calc(var(--mantine-spacing-xl) * 2);
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-8));

@media (max-width: $mantine-breakpoint-sm) {
flex-direction: column-reverse;
Expand Down Expand Up @@ -58,4 +55,7 @@
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}


.error:first-letter {
text-transform: capitalize
}
5 changes: 3 additions & 2 deletions webapp/src/AppInit/SetupBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import React from 'react';

type Props = {
onCompleted: (newType: boolean) => void;
cloudType: string;
};

export function SetupBanner({onCompleted}:Props) {
export function SetupBanner({onCompleted, cloudType}:Props) {
const [step, setStep] = useState<number>(0);
const [secret, setSecret] = useState<string>("");

Expand All @@ -18,7 +19,7 @@ export function SetupBanner({onCompleted}:Props) {
}, [step]);

if(step === 0) {
return <SetSecret onChangeStep={setStep} onChangeSecret={setSecret} />
return <SetSecret onChangeStep={setStep} onChangeSecret={setSecret} cloudType={cloudType} />
} else if(step === 1) {
return <SetAdminPassword onChangeStep={setStep} secret={secret} />
}
Expand Down

0 comments on commit cb04b34

Please sign in to comment.