Skip to content
Snippets Groups Projects
Commit 682a7ea5 authored by Matthias Feyll's avatar Matthias Feyll :cookie:
Browse files

finished visualization of login page

parent 69dd439b
No related branches found
No related tags found
1 merge request!1005Draft: feature-ui-361_setup-project
Showing
with 1448 additions and 66 deletions
# goSDN - react ui
The goSDN project is currently managed by a cli. With increased complexity it's getting harder and harder to manage, observe and debug this networks. This subproject provides an UI that keep large projects handable
## Getting started
Install all dependencies
```
yarn install
yarn build::api
```
Run the local development server
```
yarn start
```
The ui is now accessible by `localhost:3000`
## Development notes
The ui can run independently from goSDN. But to actually get in touch with the ui, log in and start working with it you need a running goSDN instance on your local maschine.
\ No newline at end of file
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.2.4", "@reduxjs/toolkit": "^2.2.4",
"@testing-library/jest-dom": "^6.4.8", "@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
......
...@@ -6,5 +6,6 @@ ...@@ -6,5 +6,6 @@
"exportName": "api", "exportName": "api",
"hooks": true, "hooks": true,
"nullSafeAdditionalProps": true, "nullSafeAdditionalProps": true,
"withInterfaces": true "withInterfaces": true,
"tag": true
} }
\ No newline at end of file
import { Button, Col, Container, Form, Image, Row } from 'react-bootstrap' import { Alert, Button, Col, Container, Form, Image, Row, Spinner } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import './login.scss' import './login.scss'
import logo from '@assets/logo.svg' import logo from '@assets/logo.svg'
import React, { useRef } from 'react'
import useLoginViewModel from '@viewmodel/login.viewmodel' import useLoginViewModel from '@viewmodel/login.viewmodel'
import React, { useRef } from 'react'
const LoginPage = () => { const LoginPage = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { valid, login, handleErrorMessageRendering } = useLoginViewModel(); const { login, handleErrorMessageRendering, displayFormFieldChecks, loginLoading } = useLoginViewModel();
const usernameRef = useRef<HTMLInputElement>(null) const usernameRef = useRef<HTMLInputElement>(null)
...@@ -22,7 +22,9 @@ const LoginPage = () => { ...@@ -22,7 +22,9 @@ const LoginPage = () => {
login(username, password); login(username, password);
} }
const invalidCredentials = (<div className="danger-box">{t('login.form.failed')}</div>) const invalidForm = (<Alert variant="warning">{t('login.form.invalid')}</Alert>)
const invalidCredentials = (<Alert variant="danger">{t('login.form.failed')}</Alert>)
return ( return (
<Container className="vh-100 d-flex flex-column justify-content-center login-container"> <Container className="vh-100 d-flex flex-column justify-content-center login-container">
...@@ -33,9 +35,9 @@ const LoginPage = () => { ...@@ -33,9 +35,9 @@ const LoginPage = () => {
<Col md={6} sm={10} className="c-box p-4"> <Col md={6} sm={10} className="c-box p-4">
<h1 className="text-center h2">goSDN - Web</h1> <h1 className="text-center h2">goSDN - Web</h1>
{handleErrorMessageRendering(invalidCredentials)} {handleErrorMessageRendering(invalidForm, invalidCredentials)}
<Form className="mt-4" noValidate validated={valid} onSubmit={triggerLogin}> <Form className="mt-4" noValidate validated={displayFormFieldChecks()} onSubmit={triggerLogin}>
<Form.Group <Form.Group
className="mb-3" className="mb-3"
controlId="loginForm.username" controlId="loginForm.username"
...@@ -65,8 +67,14 @@ const LoginPage = () => { ...@@ -65,8 +67,14 @@ const LoginPage = () => {
variant="primary" variant="primary"
type="submit" type="submit"
className="w-100 mt-3" className="w-100 mt-3"
disabled={loginLoading}
> >
{t('global.form.submit')} {t('global.form.submit')}
{loginLoading &&
<Spinner animation="border" size="sm" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
}
</Button> </Button>
</Form> </Form>
</Col> </Col>
......
import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api"; import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api";
import { LoginFormFields, setLoginFormFields, test } from '@reducer/login.reducer';
import { setToken } from "@reducer/user.reducer"; import { setToken } from "@reducer/user.reducer";
import { useDispatch, useSelector } from "react-redux"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { AppDispatch, RootState } from "src/stores"; import { useDispatch } from "react-redux";
import { AppDispatch } from "src/stores";
export interface PageLoginState {
submitted: boolean,
valid: boolean,
}
export default function useLoginViewModel() { export default function useLoginViewModel() {
const { form, backendResponse } = useSelector((state: RootState) => state.loginPageReducer);
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
const [ const [
sendLogin, sendLogin,
{ data: loginResponse, error: loginError, isLoading: loginLoading, reset: resetLogin }
] = useAuthServiceLoginMutation() ] = useAuthServiceLoginMutation()
const handleErrorMessageRendering = (renderError: JSX.Element): JSX.Element | null => { const [localFormState, updateLocalFormState] = useState({
if (form.valid && !backendResponse.valid) { submitted: false,
return renderError; valid: false,
});
useEffect(() => {
console.log('loginREsponse:' + loginResponse);
}, [loginResponse])
const handleErrorMessageRendering = (formInvalidError: JSX.Element, backendResponseError: JSX.Element): JSX.Element | null => {
// backend response check
console.log('loginError:' + loginError);
if (!!loginError) {
return backendResponseError;
}
// form invalid check
if (localFormState.submitted && !localFormState.valid) {
return formInvalidError;
} }
return null; return null;
...@@ -36,6 +58,10 @@ export default function useLoginViewModel() { ...@@ -36,6 +58,10 @@ export default function useLoginViewModel() {
return payload; return payload;
} }
const isFormValid = (username: string | undefined, password: string | undefined): boolean => {
return !!username && !!password;
}
/** /**
* Tries to `/login` by using the input fields. * Tries to `/login` by using the input fields.
* *
...@@ -43,15 +69,14 @@ export default function useLoginViewModel() { ...@@ -43,15 +69,14 @@ export default function useLoginViewModel() {
* @param event Submit event * @param event Submit event
*/ */
const loginHandler = (username: string | undefined, password: string | undefined) => { const loginHandler = (username: string | undefined, password: string | undefined) => {
const loginFormFields: LoginFormFields = { resetLogin();
username: username, const valid = isFormValid(username, password);
password: password
}
dispatch(setLoginFormFields(loginFormFields)); updateLocalFormState({ ...localFormState, valid, submitted: true })
// don´t execute it here, execute it by subscribing to the store if (valid) {
//executeLogin(username!, password!); executeLogin(username!, password!);
}
} }
const executeLogin = (username: string, password: string) => { const executeLogin = (username: string, password: string) => {
...@@ -69,10 +94,15 @@ export default function useLoginViewModel() { ...@@ -69,10 +94,15 @@ export default function useLoginViewModel() {
}); });
} }
const displayFormFieldChecks = (): boolean => {
return localFormState.submitted && !loginError;
}
return { return {
displayFormFieldChecks,
login: loginHandler, login: loginHandler,
valid: backendResponse.valid, handleErrorMessageRendering: handleErrorMessageRendering,
handleErrorMessageRendering loginLoading,
} }
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"login": { "login": {
"form": { "form": {
"failed": "The username or password is invalid", "failed": "The username or password is invalid",
"invalid": "Please type a username and password",
"username": { "username": {
"label": "Username" "label": "Username"
}, },
......
import './index.scss'
import React from 'react' import React from 'react'
import ReactDOM, { Container } from 'react-dom/client' import ReactDOM, { Container } from 'react-dom/client'
import { import {
...@@ -7,7 +8,6 @@ import { ...@@ -7,7 +8,6 @@ import {
createBrowserRouter, createBrowserRouter,
createRoutesFromElements, createRoutesFromElements,
} from 'react-router-dom' } from 'react-router-dom'
import './index.scss'
import Landingpage from './components/view/landingpage/landingpage' import Landingpage from './components/view/landingpage/landingpage'
import LoginPage from './components/view/login/login' import LoginPage from './components/view/login/login'
...@@ -18,6 +18,8 @@ import { Provider } from 'react-redux' ...@@ -18,6 +18,8 @@ import { Provider } from 'react-redux'
import { persistor, store } from './stores' import { persistor, store } from './stores'
import { ToastContainer } from 'react-toastify' import { ToastContainer } from 'react-toastify'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import './utils/icons/icons';
const root = ReactDOM.createRoot(document.getElementById('root') as Container) const root = ReactDOM.createRoot(document.getElementById('root') as Container)
......
import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { startListening } from '../middleware/listener.middleware'; import { startListening } from '../middleware/listener.middleware';
import { useAuthServiceLoginMutation } from '@api/api'; import { AuthServiceLoginApiArg, RbacUser, api, useAuthServiceLoginMutation } from '@api/api';
export interface LoginFormFields { const initialState = {
username?: string,
password?: string
}
export interface PageLoginState {
form: {
valid: boolean,
fields: LoginFormFields
},
backendResponse: {
valid: boolean,
send: boolean
}
}
const initialState: PageLoginState = {
backendResponse: { backendResponse: {
valid: false, valid: false,
send: false send: false
...@@ -37,28 +21,29 @@ const loginSlice = createSlice({ ...@@ -37,28 +21,29 @@ const loginSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setLoginBackendCheck: (state, action: PayloadAction<boolean>) => { state.backendResponse.send = action.payload }, setLoginBackendCheck: (state, action: PayloadAction<boolean>) => { state.backendResponse.send = action.payload },
setLoginFormFields: (state, action: PayloadAction<LoginFormFields>) => {
const valid = !!action.payload.username && !!action.payload.password;
state.form.valid = valid;
if (valid) { loginSuccess: (state, action: PayloadAction<string>) => {
state.form.fields = action.payload state.backendResponse.valid = true;
} }
},
}, },
}) })
export const { setLoginBackendCheck } = loginSlice.actions export const { setLoginBackendCheck } = loginSlice.actions
export const { setLoginFormFields } = loginSlice.actions
export default loginSlice.reducer export default loginSlice.reducer
startListening({ // startListening({
actionCreator: setLoginFormFields, // actionCreator: setLoginFormFields,
effect: async (action, listenerApi) => { // effect: async (action, listenerApi) => {
console.log('oh baby'); // const payload: AuthServiceLoginApiArg = {
// rbacLoginRequest: {
}, // username: action.payload.username,
}) // pwd: action.payload.password,
\ No newline at end of file // timestamp: new Date().getTime().toString(),
// },
// }
// listenerApi.dispatch(api.endpoints.authServiceLogin.initiate(payload));
// },
// })
\ No newline at end of file
import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RbacUser } from '../../utils/api/api';
export interface UserSliceState { export interface UserSliceState {
token: string, token: string,
user: RbacUser | null,
} }
const initialState: UserSliceState = { const initialState: UserSliceState = {
token: '' token: '',
user: null
} }
......
...@@ -11,10 +11,16 @@ $border-radius: 10px; ...@@ -11,10 +11,16 @@ $border-radius: 10px;
border-radius: $border-radius; border-radius: $border-radius;
} }
.danger-box { .abstract-box {
background-color: map-get($theme-colors, 'danger') !important;
border-radius: calc($border-radius / 2);
padding: 16px $box-padding; padding: 16px $box-padding;
font-size: .90em; font-size: .90em;
} border-radius: calc($border-radius / 2);
\ No newline at end of file }
// @each $color, $value in $theme-colors {
// .#{$color}-box {
// @extend .abstract-box;
// background-color: $value !important;
// }
// }
...@@ -2,6 +2,7 @@ $theme-colors: ( ...@@ -2,6 +2,7 @@ $theme-colors: (
'primary': #b350e0, 'primary': #b350e0,
'bg-primary': #E1E1E1, 'bg-primary': #E1E1E1,
'danger': #ffdcdc, 'danger': #ffdcdc,
'warning': #dbd116,
); );
@import '/node_modules/bootstrap/scss/bootstrap'; @import '/node_modules/bootstrap/scss/bootstrap';
This diff is collapsed.
import { library } from '@fortawesome/fontawesome-svg-core'
import { faSpinner, fas } from '@fortawesome/free-solid-svg-icons'
library.add(fas, faSpinner)
\ No newline at end of file
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@assets/*": ["assets/*"], "@assets/*": ["assets/*"],
"@api/*": ["src/api/*"], "@api/*": ["src/utils/api/*"],
"@viewmodel/*": ["src/components/view_model/*"], "@viewmodel/*": ["src/components/view_model/*"],
"@view/*": ["src/components/view/*"], "@view/*": ["src/components/view/*"],
"@reducer/*": ["src/stores/reducer/*"], "@reducer/*": ["src/stores/reducer/*"],
......
...@@ -29,7 +29,7 @@ export default defineConfig({ ...@@ -29,7 +29,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@assets': '/assets', '@assets': '/assets',
'@api': '/src/api', '@api': '/src/utils/api',
"@viewmodel": "/src/components/view_model", "@viewmodel": "/src/components/view_model",
"@view": "/src/components/view", "@view": "/src/components/view",
"@reducer": "/src/stores/reducer", "@reducer": "/src/stores/reducer",
......
...@@ -1494,6 +1494,39 @@ ...@@ -1494,6 +1494,39 @@
resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.3.0.tgz#731656abe21e8e769a7f70a4d833e6312fe59b7f" resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.3.0.tgz#731656abe21e8e769a7f70a4d833e6312fe59b7f"
integrity sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw== integrity sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==
"@fortawesome/fontawesome-common-types@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f"
integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==
"@fortawesome/fontawesome-svg-core@^6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff"
integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==
dependencies:
"@fortawesome/fontawesome-common-types" "6.6.0"
"@fortawesome/free-regular-svg-icons@^6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz#fc49a947ac8dfd20403c9ea5f37f0919425bdf04"
integrity sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==
dependencies:
"@fortawesome/fontawesome-common-types" "6.6.0"
"@fortawesome/free-solid-svg-icons@^6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz#061751ca43be4c4d814f0adbda8f006164ec9f3b"
integrity sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==
dependencies:
"@fortawesome/fontawesome-common-types" "6.6.0"
"@fortawesome/react-fontawesome@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4"
integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==
dependencies:
prop-types "^15.8.1"
"@humanwhocodes/config-array@^0.11.14": "@humanwhocodes/config-array@^0.11.14":
version "0.11.14" version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment