Generated project

This commit is contained in:
start.vaadin.com
2021-07-25 10:44:30 +00:00
commit 6e3f448aa3
50 changed files with 10663 additions and 0 deletions

74
frontend/auth.ts Normal file
View File

@@ -0,0 +1,74 @@
import { login as loginImpl, LoginResult, logout as logoutImpl } from '@vaadin/flow-frontend';
import { appStore } from './stores/app-store';
interface Authentication {
timestamp: number;
}
let authentication: Authentication | undefined = undefined;
const AUTHENTICATION_KEY = 'authentication';
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
// Get authentication from local storage
const storedAuthenticationJson = localStorage.getItem(AUTHENTICATION_KEY);
if (storedAuthenticationJson !== null) {
const storedAuthentication = JSON.parse(storedAuthenticationJson) as Authentication;
// Check that the stored timestamp is not older than 30 days
const hasRecentAuthenticationTimestamp = new Date().getTime() - storedAuthentication.timestamp < THIRTY_DAYS_MS;
if (hasRecentAuthenticationTimestamp) {
// Use loaded authentication
authentication = storedAuthentication;
} else {
// Delete expired stored authentication
setSessionExpired();
}
}
/**
* Forces the session to expire and removes user information stored in
* `localStorage`.
*/
export function setSessionExpired() {
authentication = undefined;
// Delete the authentication from the local storage
localStorage.removeItem(AUTHENTICATION_KEY);
}
export function isAuthenticated() {
return !!authentication;
}
/**
* Login wrapper method that retrieves user information.
*
* Uses `localStorage` for offline support.
*/
export async function login(username: string, password: string): Promise<LoginResult> {
const result = await loginImpl(username, password);
if (!result.error) {
// Get user info from endpoint
await appStore.fetchUserInfo();
authentication = {
timestamp: new Date().getTime(),
};
// Save the authentication to local storage
localStorage.setItem(AUTHENTICATION_KEY, JSON.stringify(authentication));
}
return result;
}
/**
* Login wrapper method that retrieves user information.
*
* Uses `localStorage` for offline support.
*/
export async function logout() {
setSessionExpired();
await logoutImpl();
appStore.clearUserInfo();
location.href = '';
}

24
frontend/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fusion Management</title>
<style>
body {
margin: 0;
width: 100vw;
height: 100vh;
}
#outlet {
height: 100%;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<div id="outlet"></div>
</body>
</html>

20
frontend/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import { Router } from '@vaadin/router';
import { routes } from './routes';
import { appStore } from './stores/app-store';
export const router = new Router(document.querySelector('#outlet'));
appStore.fetchUserInfo().finally(() => {
// Ensure router access checks are not done before we know if we are logged in
router.setRoutes(routes);
});
window.addEventListener('vaadin-router-location-changed', (e) => {
appStore.setLocation((e as CustomEvent).detail.location);
const title = appStore.currentViewTitle;
if (title) {
document.title = title + ' | ' + appStore.applicationName;
} else {
document.title = appStore.applicationName;
}
});

80
frontend/routes.ts Normal file
View File

@@ -0,0 +1,80 @@
import { Route } from '@vaadin/router';
import Role from './generated/com/example/application/data/Role';
import { appStore } from './stores/app-store';
import './views/home/home-view';
import './views/main-layout';
export type ViewRoute = Route & {
title?: string;
icon?: string;
requiresLogin?: boolean;
rolesAllowed?: Role[];
children?: ViewRoute[];
};
export const hasAccess = (route: Route) => {
const viewRoute = route as ViewRoute;
if (viewRoute.requiresLogin && !appStore.loggedIn) {
return false;
}
if (viewRoute.rolesAllowed) {
return viewRoute.rolesAllowed.some((role) => appStore.isUserInRole(role));
}
return true;
};
export const views: ViewRoute[] = [
// place routes below (more info https://vaadin.com/docs/latest/fusion/routing/overview)
{
path: '',
component: 'home-view',
icon: 'la la-home',
title: 'Home',
},
{
path: 'profile',
component: 'profile-view',
requiresLogin: true,
icon: 'la la-user',
title: 'Profile',
action: async (_context, _command) => {
if (!hasAccess(_context.route)) {
return _command.redirect('login');
}
await import('./views/profile/profile-view');
return;
},
},
{
path: 'admin',
component: 'admin-view',
rolesAllowed: [Role.ADMIN],
icon: 'la la-user-lock',
title: 'Admin',
action: async (_context, _command) => {
if (!hasAccess(_context.route)) {
return _command.redirect('login');
}
await import('./views/admin/admin-view');
return;
},
},
];
export const routes: ViewRoute[] = [
{
path: '',
component: 'main-layout',
children: [...views],
},
{
path: 'login',
component: 'login-view',
icon: '',
title: 'Login',
action: async (_context, _command) => {
await import('./views/login/login-view');
return;
},
},
];

View File

@@ -0,0 +1,48 @@
import { RouterLocation } from '@vaadin/router';
import User from 'Frontend/generated/com/cubetiqs/fusion/data/entity/User';
import Role from 'Frontend/generated/com/cubetiqs/fusion/data/Role';
import { UserEndpoint } from 'Frontend/generated/UserEndpoint';
import { makeAutoObservable } from 'mobx';
export class AppStore {
applicationName = 'Fusion Management';
// The location, relative to the base path, e.g. "hello" when viewing "/hello"
location = '';
currentViewTitle = '';
user: User | undefined = undefined;
constructor() {
makeAutoObservable(this);
}
setLocation(location: RouterLocation) {
if (location.route) {
this.location = location.route.path;
} else if (location.pathname.startsWith(location.baseUrl)) {
this.location = location.pathname.substr(location.baseUrl.length);
} else {
this.location = location.pathname;
}
this.currentViewTitle = (location?.route as any)?.title || '';
}
async fetchUserInfo() {
this.user = await UserEndpoint.getAuthenticatedUser();
}
clearUserInfo() {
this.user = undefined;
}
get loggedIn() {
return !!this.user;
}
isUserInRole(role: Role) {
return this.user?.roles?.includes(role);
}
}
export const appStore = new AppStore();

View File

@@ -0,0 +1,50 @@
.sidemenu-header {
align-items: center;
box-shadow: var(--lumo-box-shadow-s);
display: flex;
height: var(--lumo-size-xl);
width: 100%;
}
.sidemenu-header h1 {
font-size: var(--lumo-font-size-l);
margin: 0;
}
.sidemenu-menu #logo {
align-items: center;
box-sizing: border-box;
display: flex;
padding: var(--lumo-space-s) var(--lumo-space-m);
}
.sidemenu-menu #logo img {
height: calc(var(--lumo-size-l) * 1.5);
}
.sidemenu-menu #logo span {
font-size: var(--lumo-font-size-xl);
font-weight: 600;
margin: 0 var(--lumo-space-s);
}
.sidemenu-menu vaadin-tab {
font-size: var(--lumo-font-size-s);
height: var(--lumo-size-l);
font-weight: 600;
color: var(--lumo-body-text-color);
}
.sidemenu-menu vaadin-tab:hover {
background-color: var(--lumo-contrast-5pct);
text-decoration: none;
}
.sidemenu-menu vaadin-tab[selected] {
background-color: var(--lumo-primary-color-10pct);
color: var(--lumo-primary-text-color);
}
hr {
margin: 0;
}

View File

@@ -0,0 +1,9 @@
@import url('./main-layout.css');
@import url('./views/home-view.css');
@import url('./views/profile-view.css');
@import url('./views/admin-view.css');
@import url('line-awesome/dist/line-awesome/css/line-awesome.min.css');
@import url('@fontsource/roboto'); html {
--lumo-font-family: Roboto;
}

View File

@@ -0,0 +1 @@
{"lumoImports":["typography","color","spacing","badge","utility"]}

View File

@@ -0,0 +1,3 @@
admin-view {
display: block;
}

View File

@@ -0,0 +1,3 @@
home-view {
display: block;
}

View File

@@ -0,0 +1,3 @@
profile-view {
display: block;
}

View File

@@ -0,0 +1,10 @@
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from '../../views/view';
@customElement('admin-view')
export class AdminView extends View {
render() {
return html`<div>Content placeholder</div>`;
}
}

View File

@@ -0,0 +1,10 @@
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from '../../views/view';
@customElement('home-view')
export class HomeView extends View {
render() {
return html`<div>Content placeholder</div>`;
}
}

View File

@@ -0,0 +1,40 @@
import '@vaadin/vaadin-login';
import { LoginI18n } from '@vaadin/vaadin-login';
import { html } from 'lit';
import { customElement, state } from 'lit/decorators';
import { View } from '../../views/view';
const loginI18nDefault: LoginI18n = {
form: {
title: 'Log in',
username: 'Username',
password: 'Password',
submit: 'Log in',
forgotPassword: 'Forgot password',
},
errorMessage: {
title: 'Incorrect username or password',
message: 'Check that you have entered the correct username and password and try again.',
},
};
@customElement('login-view')
export class LoginView extends View {
@state()
private error = false;
render() {
return html`
<vaadin-login-overlay
opened
.error=${this.error}
action="login"
no-forgot-password
.i18n=${Object.assign(
{ header: { title: 'Fusion Management', description: 'Login using user/user or admin/admin' } },
loginI18nDefault
)}
>
</vaadin-login-overlay>
`;
}
}

View File

@@ -0,0 +1,83 @@
import '@vaadin/vaadin-app-layout';
import { AppLayoutElement } from '@vaadin/vaadin-app-layout';
import '@vaadin/vaadin-app-layout/vaadin-drawer-toggle';
import '@vaadin/vaadin-avatar/vaadin-avatar';
import '@vaadin/vaadin-context-menu';
import '@vaadin/vaadin-tabs';
import '@vaadin/vaadin-tabs/vaadin-tab';
import '@vaadin/vaadin-template-renderer';
import { html, render } from 'lit';
import { customElement } from 'lit/decorators.js';
import { logout } from '../auth';
import { router } from '../index';
import { hasAccess, views } from '../routes';
import { appStore } from '../stores/app-store';
import { Layout } from './view';
interface RouteInfo {
path: string;
title: string;
icon: string;
}
@customElement('main-layout')
export class MainLayout extends Layout {
render() {
return html`
<vaadin-app-layout primary-section="drawer">
<header slot="navbar" theme="dark" class="sidemenu-header">
<vaadin-drawer-toggle></vaadin-drawer-toggle>
<h1>${appStore.currentViewTitle}</h1>
${appStore.user
? html` <vaadin-context-menu class="ms-auto me-m" open-on="click" .renderer="${this.renderLogoutOptions}">
<vaadin-avatar img="${appStore.user.profilePictureUrl}" name="${appStore.user.name}"></vaadin-avatar
></vaadin-context-menu>`
: html`<a class="ms-auto me-m" router-ignore href="login">Sign in</a>`}
</header>
<div slot="drawer" class="sidemenu-menu">
<div id="logo">
<img src="images/logo.png" alt="${appStore.applicationName} logo" />
<span>${appStore.applicationName}</span>
</div>
<hr />
<vaadin-tabs orientation="vertical" theme="minimal" .selected=${this.getSelectedViewRoute()}>
${this.getMenuRoutes().map(
(viewRoute) => html`
<vaadin-tab>
<span class="${viewRoute.icon} pr-s"></span>
<a href=${router.urlForPath(viewRoute.path)} tabindex="-1">${viewRoute.title}</a>
</vaadin-tab>
`
)}
</vaadin-tabs>
</div>
<slot></slot>
</vaadin-app-layout>
`;
}
connectedCallback() {
super.connectedCallback();
this.classList.add('block', 'h-full');
this.reaction(
() => appStore.location,
() => {
AppLayoutElement.dispatchCloseOverlayDrawerEvent();
}
);
}
private renderLogoutOptions(root: HTMLElement) {
render(html`<vaadin-list-box><vaadin-item @click=${() => logout()}>Logout</vaadin-item></vaadin-list-box>`, root);
}
private getMenuRoutes(): RouteInfo[] {
return views.filter((route) => route.title).filter((route) => hasAccess(route)) as RouteInfo[];
}
private getSelectedViewRoute(): number {
const path = appStore.location;
return this.getMenuRoutes().findIndex((viewRoute) => viewRoute.path == path);
}
}

View File

@@ -0,0 +1,10 @@
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from '../../views/view';
@customElement('profile-view')
export class ProfileView extends View {
render() {
return html`<div>Content placeholder</div>`;
}
}

51
frontend/views/view.ts Normal file
View File

@@ -0,0 +1,51 @@
import { MobxLitElement } from '@adobe/lit-mobx';
import { applyTheme } from 'Frontend/generated/theme';
import { autorun, IAutorunOptions, IReactionDisposer, IReactionOptions, IReactionPublic, reaction } from 'mobx';
export class MobxElement extends MobxLitElement {
private disposers: IReactionDisposer[] = [];
/**
* Creates a MobX reaction using the given parameters and disposes it when this element is detached.
*
* This should be called from `connectedCallback` to ensure that the reaction is active also if the element is attached again later.
*/
protected reaction<T>(
expression: (r: IReactionPublic) => T,
effect: (arg: T, prev: T, r: IReactionPublic) => void,
opts?: IReactionOptions
): void {
this.disposers.push(reaction(expression, effect, opts));
}
/**
* Creates a MobX autorun using the given parameters and disposes it when this element is detached.
*
* This should be called from `connectedCallback` to ensure that the reaction is active also if the element is attached again later.
*/
protected autorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions): void {
this.disposers.push(autorun(view, opts));
}
disconnectedCallback() {
super.disconnectedCallback();
this.disposers.forEach((disposer) => {
disposer();
});
this.disposers = [];
}
}
export class View extends MobxElement {
createRenderRoot() {
// Do not use a shadow root
return this;
}
}
export class Layout extends MobxElement {
connectedCallback() {
super.connectedCallback();
applyTheme(this.shadowRoot!);
}
}