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 @@
"version": "0.1.0",
"private": true,
"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",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
......
......@@ -6,5 +6,6 @@
"exportName": "api",
"hooks": 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 './login.scss'
import logo from '@assets/logo.svg'
import React, { useRef } from 'react'
import useLoginViewModel from '@viewmodel/login.viewmodel'
import React, { useRef } from 'react'
const LoginPage = () => {
const { t } = useTranslation('common')
const { valid, login, handleErrorMessageRendering } = useLoginViewModel();
const { login, handleErrorMessageRendering, displayFormFieldChecks, loginLoading } = useLoginViewModel();
const usernameRef = useRef<HTMLInputElement>(null)
......@@ -22,7 +22,9 @@ const LoginPage = () => {
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 (
<Container className="vh-100 d-flex flex-column justify-content-center login-container">
......@@ -33,9 +35,9 @@ const LoginPage = () => {
<Col md={6} sm={10} className="c-box p-4">
<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
className="mb-3"
controlId="loginForm.username"
......@@ -65,8 +67,14 @@ const LoginPage = () => {
variant="primary"
type="submit"
className="w-100 mt-3"
disabled={loginLoading}
>
{t('global.form.submit')}
{loginLoading &&
<Spinner animation="border" size="sm" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
}
</Button>
</Form>
</Col>
......
import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api";
import { LoginFormFields, setLoginFormFields, test } from '@reducer/login.reducer';
import { setToken } from "@reducer/user.reducer";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "src/stores";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDispatch } from "react-redux";
import { AppDispatch } from "src/stores";
export interface PageLoginState {
submitted: boolean,
valid: boolean,
}
export default function useLoginViewModel() {
const { form, backendResponse } = useSelector((state: RootState) => state.loginPageReducer);
const dispatch = useDispatch<AppDispatch>();
const [
sendLogin,
{ data: loginResponse, error: loginError, isLoading: loginLoading, reset: resetLogin }
] = useAuthServiceLoginMutation()
const handleErrorMessageRendering = (renderError: JSX.Element): JSX.Element | null => {
if (form.valid && !backendResponse.valid) {
return renderError;
const [localFormState, updateLocalFormState] = useState({
submitted: false,
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;
......@@ -36,6 +58,10 @@ export default function useLoginViewModel() {
return payload;
}
const isFormValid = (username: string | undefined, password: string | undefined): boolean => {
return !!username && !!password;
}
/**
* Tries to `/login` by using the input fields.
*
......@@ -43,15 +69,14 @@ export default function useLoginViewModel() {
* @param event Submit event
*/
const loginHandler = (username: string | undefined, password: string | undefined) => {
const loginFormFields: LoginFormFields = {
username: username,
password: password
}
resetLogin();
const valid = isFormValid(username, password);
dispatch(setLoginFormFields(loginFormFields));
updateLocalFormState({ ...localFormState, valid, submitted: true })
// don´t execute it here, execute it by subscribing to the store
//executeLogin(username!, password!);
if (valid) {
executeLogin(username!, password!);
}
}
const executeLogin = (username: string, password: string) => {
......@@ -69,10 +94,15 @@ export default function useLoginViewModel() {
});
}
const displayFormFieldChecks = (): boolean => {
return localFormState.submitted && !loginError;
}
return {
displayFormFieldChecks,
login: loginHandler,
valid: backendResponse.valid,
handleErrorMessageRendering
handleErrorMessageRendering: handleErrorMessageRendering,
loginLoading,
}
}
......@@ -9,6 +9,7 @@
"login": {
"form": {
"failed": "The username or password is invalid",
"invalid": "Please type a username and password",
"username": {
"label": "Username"
},
......
import './index.scss'
import React from 'react'
import ReactDOM, { Container } from 'react-dom/client'
import {
......@@ -7,7 +8,6 @@ import {
createBrowserRouter,
createRoutesFromElements,
} from 'react-router-dom'
import './index.scss'
import Landingpage from './components/view/landingpage/landingpage'
import LoginPage from './components/view/login/login'
......@@ -18,6 +18,8 @@ import { Provider } from 'react-redux'
import { persistor, store } from './stores'
import { ToastContainer } from 'react-toastify'
import { PersistGate } from 'redux-persist/integration/react'
import './utils/icons/icons';
const root = ReactDOM.createRoot(document.getElementById('root') as Container)
......
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { startListening } from '../middleware/listener.middleware';
import { useAuthServiceLoginMutation } from '@api/api';
import { AuthServiceLoginApiArg, RbacUser, api, useAuthServiceLoginMutation } from '@api/api';
export interface LoginFormFields {
username?: string,
password?: string
}
export interface PageLoginState {
form: {
valid: boolean,
fields: LoginFormFields
},
backendResponse: {
valid: boolean,
send: boolean
}
}
const initialState: PageLoginState = {
const initialState = {
backendResponse: {
valid: false,
send: false
......@@ -37,28 +21,29 @@ const loginSlice = createSlice({
initialState,
reducers: {
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) {
state.form.fields = action.payload
}
},
loginSuccess: (state, action: PayloadAction<string>) => {
state.backendResponse.valid = true;
}
},
})
export const { setLoginBackendCheck } = loginSlice.actions
export const { setLoginFormFields } = loginSlice.actions
export default loginSlice.reducer
startListening({
actionCreator: setLoginFormFields,
effect: async (action, listenerApi) => {
console.log('oh baby');
},
})
\ No newline at end of file
// startListening({
// actionCreator: setLoginFormFields,
// effect: async (action, listenerApi) => {
// const payload: AuthServiceLoginApiArg = {
// rbacLoginRequest: {
// username: action.payload.username,
// pwd: action.payload.password,
// 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 { RbacUser } from '../../utils/api/api';
export interface UserSliceState {
token: string,
user: RbacUser | null,
}
const initialState: UserSliceState = {
token: ''
token: '',
user: null
}
......
......@@ -11,10 +11,16 @@ $border-radius: 10px;
border-radius: $border-radius;
}
.danger-box {
background-color: map-get($theme-colors, 'danger') !important;
border-radius: calc($border-radius / 2);
.abstract-box {
padding: 16px $box-padding;
font-size: .90em;
}
\ No newline at end of file
border-radius: calc($border-radius / 2);
}
// @each $color, $value in $theme-colors {
// .#{$color}-box {
// @extend .abstract-box;
// background-color: $value !important;
// }
// }
......@@ -2,6 +2,7 @@ $theme-colors: (
'primary': #b350e0,
'bg-primary': #E1E1E1,
'danger': #ffdcdc,
'warning': #dbd116,
);
@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 @@
"baseUrl": ".",
"paths": {
"@assets/*": ["assets/*"],
"@api/*": ["src/api/*"],
"@api/*": ["src/utils/api/*"],
"@viewmodel/*": ["src/components/view_model/*"],
"@view/*": ["src/components/view/*"],
"@reducer/*": ["src/stores/reducer/*"],
......
......@@ -29,7 +29,7 @@ export default defineConfig({
resolve: {
alias: {
'@assets': '/assets',
'@api': '/src/api',
'@api': '/src/utils/api',
"@viewmodel": "/src/components/view_model",
"@view": "/src/components/view",
"@reducer": "/src/stores/reducer",
......
......@@ -1494,6 +1494,39 @@
resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.3.0.tgz#731656abe21e8e769a7f70a4d833e6312fe59b7f"
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":
version "0.11.14"
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