🪇 Implements major structure and service refactors
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import InputPassword from '$lib/components/InputPassword.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import Login from '~icons/tabler/login';
|
||||
import { api } from '$lib/api';
|
||||
import type { JWT } from '$lib/models';
|
||||
|
||||
type SignInData = {
|
||||
password: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
|
||||
let loginFailed = false;
|
||||
|
||||
let token = { access_token: '' };
|
||||
|
||||
async function signInUser(data: SignInData) {
|
||||
const result = await api.post<JWT>('/api/signIn', data)
|
||||
if (result.isErr()){
|
||||
username = '';
|
||||
password = '';
|
||||
notifications.error('Wrong Username or Password!', 5000);
|
||||
loginFailed = true;
|
||||
setTimeout(() => {
|
||||
loginFailed = false;
|
||||
}, 1500);
|
||||
return
|
||||
}
|
||||
token = result.inner;
|
||||
user.init(token.access_token);
|
||||
username = $user.username;
|
||||
notifications.success('User ' + username + ' signed in', 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hero from-primary/30 to-secondary/30 min-h-screen bg-gradient-to-br">
|
||||
<div
|
||||
class="card lg:card-side bg-base-100 face shadow-2xl {loginFailed
|
||||
? 'failure border-error border-2'
|
||||
: ''}"
|
||||
in:fly={{ delay: 200, y: 100, duration: 500 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<figure class="bg-base-200"><img src={logo} alt="Logo" class="h-auto w-48 lg:w-64" /></figure>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-2xl">Login</h2>
|
||||
<form class="form-control w-full max-w-xs">
|
||||
<label class="label" for="user">
|
||||
<span class="label-text text-md">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="user"
|
||||
bind:value={username}
|
||||
/>
|
||||
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<InputPassword id="pwd" bind:value={password} />
|
||||
|
||||
<div class="card-actions mt-4 justify-end">
|
||||
<button
|
||||
class="btn btn-primary inline-flex items-center"
|
||||
on:click={() => {
|
||||
signInUser({ username, password });
|
||||
}}><Login class="mr-2 h-5 w-5" /><span>Login</span></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.failure {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
@keyframes shake {
|
||||
10%,
|
||||
90% {
|
||||
transform: translatex(-1px);
|
||||
}
|
||||
|
||||
20%,
|
||||
80% {
|
||||
transform: translatex(2px);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translatex(-4px);
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
transform: translatex(4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,279 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import MdiGithub from '~icons/mdi/github';
|
||||
import MdiConnection from '~icons/mdi/connection';
|
||||
import Users from '~icons/mdi/users';
|
||||
import Settings from '~icons/mdi/settings';
|
||||
import MdiController from '~icons/mdi/controller';
|
||||
import Devices from '~icons/mdi/devices'
|
||||
import Camera from '~icons/mdi/camera-outline';
|
||||
import Rotate3d from '~icons/mdi/rotate-3d';
|
||||
import MotorOutline from '~icons/mdi/motor-outline';
|
||||
import Health from '~icons/mdi/stethoscope';
|
||||
import Folder from '~icons/mdi/folder-outline';
|
||||
import Update from '~icons/mdi/reload';
|
||||
import WiFi from '~icons/mdi/wifi';
|
||||
import Router from '~icons/mdi/router';
|
||||
import AP from '~icons/mdi/access-point';
|
||||
import Remote from '~icons/mdi/network';
|
||||
import Avatar from '~icons/mdi/user-circle';
|
||||
import Logout from '~icons/mdi/logout';
|
||||
import Copyright from '~icons/mdi/copyright';
|
||||
import NTP from '~icons/mdi/clock-check';
|
||||
import Metrics from '~icons/mdi/report-bar';
|
||||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const appName = $page.data.app_name;
|
||||
|
||||
const copyright = $page.data.copyright;
|
||||
|
||||
const github = { href: 'https://github.com/' + $page.data.github, active: true };
|
||||
|
||||
type menuItem = {
|
||||
title: string;
|
||||
icon: ConstructorOfATypedSvelteComponent;
|
||||
href?: string;
|
||||
feature: boolean;
|
||||
active?: boolean;
|
||||
submenu?: subMenuItem[];
|
||||
};
|
||||
|
||||
type subMenuItem = {
|
||||
title: string;
|
||||
icon: ConstructorOfATypedSvelteComponent;
|
||||
href: string;
|
||||
feature: boolean;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
let menuItems = [
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: '/controller',
|
||||
feature: true,
|
||||
},
|
||||
{
|
||||
title: 'Peripherals',
|
||||
icon: Devices,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'I2C',
|
||||
icon: MdiConnection,
|
||||
href: '/peripherals/i2c',
|
||||
feature: true,
|
||||
},
|
||||
{
|
||||
title: 'Camera',
|
||||
icon: Camera,
|
||||
href: '/peripherals/camera',
|
||||
feature: $features.camera,
|
||||
},
|
||||
{
|
||||
title: 'Servo',
|
||||
icon: MotorOutline,
|
||||
href: '/peripherals/servo',
|
||||
feature: true,
|
||||
},
|
||||
{
|
||||
title: 'IMU',
|
||||
icon: Rotate3d,
|
||||
href: '/peripherals/imu',
|
||||
feature: $features.imu || $features.mag || $features.bmp,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Connections',
|
||||
icon: Remote,
|
||||
feature: $features.ntp,
|
||||
submenu: [
|
||||
{
|
||||
title: 'NTP',
|
||||
icon: NTP,
|
||||
href: '/connections/ntp',
|
||||
feature: $features.ntp,
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
icon: WiFi,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: '/wifi/sta',
|
||||
feature: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: '/wifi/ap',
|
||||
feature: true,
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
icon: Users,
|
||||
href: '/user',
|
||||
feature: $features.security && $user.admin,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
icon: Settings,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: '/system/status',
|
||||
feature: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'File System',
|
||||
icon: Folder,
|
||||
href: '/system/filesystem',
|
||||
feature: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: '/system/metrics',
|
||||
feature: $features.analytics,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: '/system/update',
|
||||
feature:
|
||||
($features.ota ||
|
||||
$features.upload_firmware ||
|
||||
$features.download_firmware) &&
|
||||
(!$features.security || $user.admin),
|
||||
}
|
||||
]
|
||||
}
|
||||
] as menuItem[];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function setActiveMenuItem(targetTitle: string) {
|
||||
menuItems.forEach(item => {
|
||||
item.active = item.title === targetTitle;
|
||||
item.submenu?.forEach(subItem => {
|
||||
subItem.active = subItem.title === targetTitle;
|
||||
});
|
||||
});
|
||||
menuItems = menuItems
|
||||
dispatch('menuClicked');
|
||||
}
|
||||
|
||||
$: setActiveMenuItem($page.data.title);
|
||||
</script>
|
||||
|
||||
<div class="bg-base-200 text-base-content flex h-full w-80 flex-col p-4">
|
||||
<!-- Sidebar content here -->
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
|
||||
on:click={() => setActiveMenuItem('')}
|
||||
>
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||
</a>
|
||||
<ul class="menu rounded-box menu-vertical flex-nowrap overflow-y-auto">
|
||||
{#each menuItems as menuItem, i (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
<li>
|
||||
{#if menuItem.submenu}
|
||||
<details>
|
||||
<summary class="text-lg font-bold">
|
||||
<svelte:component this={menuItem.icon} class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
<ul>
|
||||
{#each menuItem.submenu as subMenuItem}
|
||||
{#if subMenuItem.feature}
|
||||
<li class="hover-bordered">
|
||||
<a
|
||||
href={subMenuItem.href}
|
||||
class:bg-base-100={subMenuItem.active}
|
||||
class="text-ml font-bold"
|
||||
on:click={() => {
|
||||
setActiveMenuItem(subMenuItem.title);
|
||||
menuItems = menuItems;
|
||||
}}
|
||||
><svelte:component
|
||||
this={subMenuItem.icon}
|
||||
class="h-5 w-5"
|
||||
/>{subMenuItem.title}</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{:else}
|
||||
<a
|
||||
href={menuItem.href}
|
||||
class:bg-base-100={menuItem.active}
|
||||
class="text-lg font-bold"
|
||||
on:click={() => {
|
||||
setActiveMenuItem(menuItem.title);
|
||||
menuItems = menuItems;
|
||||
}}><svelte:component this={menuItem.icon} class="h-6 w-6" />{menuItem.title}</a
|
||||
>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="flex-col" />
|
||||
<div class="flex-grow" />
|
||||
|
||||
{#if $features.security}
|
||||
<div class="flex items-center">
|
||||
<Avatar class="h-8 w-8" />
|
||||
<span class="flex-grow px-4 text-xl font-bold">{$user.username}</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="btn btn-ghost"
|
||||
on:click={() => {
|
||||
user.invalidate();
|
||||
}}
|
||||
>
|
||||
<Logout class="h-8 w-8 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="divider my-0" />
|
||||
<div class="flex items-center">
|
||||
{#if github.active}
|
||||
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer"
|
||||
><MdiGithub class="h-5 w-5" /></a
|
||||
>
|
||||
{/if}
|
||||
<div class="inline-flex flex-grow items-center justify-end text-sm">
|
||||
<Copyright class="h-4 w-4" /><span class="px-2">{copyright}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { daisyColor } from "$lib/DaisyUiHelper";
|
||||
import { daisyColor } from "$lib/utilities";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import { onMount } from "svelte";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { api } from '$lib/api';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function useFeatureFlags() {
|
||||
const featureFlags = writable<Record<string, boolean>>({});
|
||||
onMount(async () => {
|
||||
const result = await api.get<Record<string, boolean>>('/api/features');
|
||||
if (result.isOk()) featureFlags.set(result.inner);
|
||||
else {
|
||||
notifications.error('Feature flag could not fetched', 2500);
|
||||
}
|
||||
});
|
||||
|
||||
return featureFlags;
|
||||
}
|
||||
+20
-29
@@ -1,36 +1,27 @@
|
||||
import { type IMU } from '$lib/types/models';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let imu_data = {
|
||||
x: <number[]>[],
|
||||
y: <number[]>[],
|
||||
z: <number[]>[],
|
||||
imu_temp: <number[]>[],
|
||||
altitude: <number[]>[],
|
||||
pressure: <number[]>[],
|
||||
bmp_temp: <number[]>[]
|
||||
};
|
||||
import type { IMU } from '$lib/types/models';
|
||||
|
||||
const maxIMUData = 100;
|
||||
|
||||
function createIMU() {
|
||||
const { subscribe, update } = writable(imu_data);
|
||||
export const imu = (() => {
|
||||
const { subscribe, update } = writable({
|
||||
x: [] as number[],
|
||||
y: [] as number[],
|
||||
z: [] as number[],
|
||||
imu_temp: [] as number[],
|
||||
altitude: [] as number[],
|
||||
pressure: [] as number[],
|
||||
bmp_temp: [] as number[]
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
addData: (content: IMU) => {
|
||||
update((imu_data) => ({
|
||||
...imu_data,
|
||||
x: [...imu_data.x, content.x].slice(-maxIMUData),
|
||||
y: [...imu_data.y, content.y].slice(-maxIMUData),
|
||||
z: [...imu_data.z, content.z].slice(-maxIMUData),
|
||||
imu_temp: [...imu_data.imu_temp, content.imu_temp].slice(-maxIMUData),
|
||||
altitude: [...imu_data.altitude, content.altitude].slice(-maxIMUData),
|
||||
pressure: [...imu_data.pressure, content.pressure].slice(-maxIMUData),
|
||||
bmp_temp: [...imu_data.bmp_temp, content.bmp_temp].slice(-maxIMUData)
|
||||
}));
|
||||
}
|
||||
const addData = (content: IMU) => {
|
||||
update((data) => {
|
||||
(Object.keys(content) as (keyof IMU)[]).forEach((key) => {
|
||||
data[key] = [...data[key], content[key]].slice(-maxIMUData);
|
||||
});
|
||||
return data;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const imu = createIMU();
|
||||
return { subscribe, addData };
|
||||
})();
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './fullscreen';
|
||||
export * from './telemetry';
|
||||
export * from './analytics';
|
||||
export * from './user';
|
||||
export * from './featureFlags';
|
||||
|
||||
+12
-27
@@ -1,55 +1,40 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
|
||||
export type userProfile = {
|
||||
export type UserProfile = {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
bearer_token: string;
|
||||
};
|
||||
|
||||
type decodedJWT = {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
};
|
||||
type DecodedJWT = Omit<UserProfile, 'bearer_token'>;
|
||||
|
||||
let empty = {
|
||||
const emptyUser: UserProfile = {
|
||||
username: '',
|
||||
admin: false,
|
||||
bearer_token: ''
|
||||
};
|
||||
|
||||
function createStore() {
|
||||
const { subscribe, set } = writable(empty);
|
||||
|
||||
// retrieve store from sessionStorage / localStorage if available
|
||||
const userdata = localStorage.getItem('user');
|
||||
if (userdata) {
|
||||
set(JSON.parse(userdata));
|
||||
}
|
||||
function createUserStore() {
|
||||
const store = persistentStore<UserProfile>('user', emptyUser);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
subscribe: store.subscribe,
|
||||
init: (access_token: string) => {
|
||||
const decoded: decodedJWT = jwtDecode(access_token);
|
||||
const userdata = {
|
||||
const decoded: DecodedJWT = jwtDecode(access_token);
|
||||
const userProfile: UserProfile = {
|
||||
bearer_token: access_token,
|
||||
username: decoded.username,
|
||||
admin: decoded.admin
|
||||
};
|
||||
set(userdata);
|
||||
// persist store in sessionStorage / localStorage
|
||||
localStorage.setItem('user', JSON.stringify(userdata));
|
||||
store.set(userProfile);
|
||||
},
|
||||
invalidate: () => {
|
||||
console.log('Log out user');
|
||||
set(empty);
|
||||
// remove localStorage "user"
|
||||
localStorage.removeItem('user');
|
||||
// redirect to login page
|
||||
store.set(emptyUser);
|
||||
goto('/');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const user = createStore();
|
||||
export const user = createUserStore();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function daisyColor(name: string, opacity: number = 100) {
|
||||
export const daisyColor = (name: string, opacity: number = 100) => {
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return `oklch(${color} / ${opacity}%)`;
|
||||
}
|
||||
};
|
||||
@@ -5,3 +5,6 @@ export * from './math-utilities';
|
||||
export * from './buffer-utilities';
|
||||
export * from './model-utilities';
|
||||
export * from './location-utilities';
|
||||
export * from './position-utilities';
|
||||
export * from './string-utilities';
|
||||
export * from './color-utilities';
|
||||
|
||||
@@ -3,14 +3,14 @@ import { browser } from '$app/environment';
|
||||
|
||||
export const isEmbeddedApp = import.meta.env.VITE_EMBEDDED_BUILD === 'true';
|
||||
|
||||
export const persistentStore = (key: string, initialValue: any) => {
|
||||
const savedValue = browser ? JSON.parse(localStorage.getItem(key) as string) : null;
|
||||
const data = savedValue !== null ? savedValue : initialValue;
|
||||
const store = writable(data);
|
||||
export const persistentStore = <T>(key: string, initialValue: T) => {
|
||||
const savedValue = browser ? localStorage.getItem(key) : null;
|
||||
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue;
|
||||
const store = writable<T>(data);
|
||||
|
||||
store.subscribe((value) => {
|
||||
browser && localStorage.setItem(key, JSON.stringify(value));
|
||||
if (browser) localStorage.setItem(key, JSON.stringify(value));
|
||||
});
|
||||
|
||||
return store;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user