diff --git a/src/components/Display.tsx b/src/components/Display.tsx index 98a6556..cde13db 100644 --- a/src/components/Display.tsx +++ b/src/components/Display.tsx @@ -1,4 +1,4 @@ -import React, { useLayoutEffect, useRef } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; import { LIGHTHOUSE_COLOR_CHANNELS, LIGHTHOUSE_COLS, @@ -7,6 +7,11 @@ import { export const DISPLAY_ASPECT_RATIO = 0.8634; +export interface MousePos { + x: number; + y: number; +} + export interface DisplayProps { frame: Uint8Array; width?: number; @@ -16,6 +21,10 @@ export interface DisplayProps { relativeBezelWidth?: number; relativeGutterWidth?: number; className?: string; + strictBoundsChecking?: boolean; + onMouseDown?: (p: MousePos) => void; + onMouseUp?: (p: MousePos) => void; + onMouseDrag?: (p: MousePos) => void; } export function Display({ @@ -27,9 +36,15 @@ export function Display({ relativeBezelWidth = 0.0183, relativeGutterWidth = 0.0064, className, + strictBoundsChecking = false, + onMouseDown = (p: MousePos) => {}, + onMouseUp = (p: MousePos) => {}, + onMouseDrag = (p: MousePos) => {}, }: DisplayProps) { const canvasRef = useRef(null); + const [drag, setDrag] = useState(false); + const [prevCoords, setPrevCoords] = useState(null); // Set up rendering useLayoutEffect(() => { const canvas = canvasRef.current!; @@ -71,18 +86,111 @@ export function Display({ ctx.fillRect(x, 0, gutterWidth, height); } + const midPoints: number[][] = []; // Draw windows for (let j = 0; j < columns; j++) { const x = bezelWidth + j * windowWidth + (j + 1) * gutterWidth; for (let i = 0; i < rows; i++) { const y = i * (1 + spacersPerRow) * windowHeight; + midPoints.push([x + windowWidth / 2, y + windowHeight / 2]); const k = (i * LIGHTHOUSE_COLS + j) * LIGHTHOUSE_COLOR_CHANNELS; const rgb = frame.slice(k, k + LIGHTHOUSE_COLOR_CHANNELS); ctx.fillStyle = `rgb(${rgb.join(',')})`; ctx.fillRect(x, y, windowWidth, windowHeight); } } + + const dist = ([x1, y1]: number[], [x2, y2]: number[]) => { + const xDiff = x1 - x2; + const yDiff = y1 - y2; + return Math.sqrt(xDiff * xDiff + yDiff * yDiff); + }; + + const mouseToWindowCoords = (mouseCoords: number[]) => { + const closestPointIdx = midPoints + .map(p => dist(p, mouseCoords)) + .reduce( + ([aIdx, acc], val, idx) => [ + val < acc ? idx : aIdx, + Math.min(acc, val), + ], + [-1, Infinity] + )[0]; + const closestPoint = midPoints[closestPointIdx]; + const x = closestPoint[0] - windowWidth / 2; + const y = closestPoint[1] - windowHeight / 2; + if ( + strictBoundsChecking && + !( + mouseCoords[0] >= x && + mouseCoords[0] <= x + windowWidth && + mouseCoords[1] >= y && + mouseCoords[1] <= y + windowHeight + ) + ) { + return null; + } + const j = Math.round( + (x - bezelWidth - gutterWidth) / (windowWidth + gutterWidth) + ); + const i = Math.round(y / (windowHeight * (1 + spacersPerRow))); + return [i, j]; + }; + + const onMouseDownHandler = (event: MouseEvent) => { + setDrag(true); + + const rect = canvas.getBoundingClientRect(); + const mouseCoords = [event.clientX - rect.left, event.clientY - rect.top]; + + const windowCoords = mouseToWindowCoords(mouseCoords); + if (!windowCoords) return; // in case of strict bounds checking + setPrevCoords(windowCoords); // for consecutive drag + + onMouseDown({ x: windowCoords[1], y: windowCoords[0] }); + }; + const onMouseUpHandler = (event: MouseEvent) => { + setDrag(false); + + const rect = canvas.getBoundingClientRect(); + const mouseCoords = [event.clientX - rect.left, event.clientY - rect.top]; + + const windowCoords = mouseToWindowCoords(mouseCoords); + if (!windowCoords) return; // in case of strict bounds checking + setPrevCoords(windowCoords); // for consecutive drag + + onMouseUp({ x: windowCoords[1], y: windowCoords[0] }); + }; + + const onMouseDragHandler = (event: MouseEvent) => { + if (!drag) return; + const rect = canvas.getBoundingClientRect(); + const mouseCoords = [event.clientX - rect.left, event.clientY - rect.top]; + + const windowCoords = mouseToWindowCoords(mouseCoords); + if (!windowCoords) return; // in case of strict bounds checking + + // don't emit drag events if coords haven't changed + if ( + prevCoords && + prevCoords[0] === windowCoords[0] && + prevCoords[1] === windowCoords[1] + ) { + return; + } + setPrevCoords(windowCoords); + onMouseDrag({ x: windowCoords[1], y: windowCoords[0] }); + }; + canvas.style.cursor = 'crosshair'; + canvas.addEventListener('mousedown', onMouseDownHandler); + canvas.addEventListener('mousemove', onMouseDragHandler); + canvas.addEventListener('mouseup', onMouseUpHandler); + return () => { + canvas.removeEventListener('mousedown', onMouseDownHandler); + canvas.removeEventListener('mousemove', onMouseDragHandler); + canvas.removeEventListener('mouseup', onMouseUpHandler); + }; }, [ customWidth, aspectRatio, @@ -91,6 +199,14 @@ export function Display({ columns, relativeBezelWidth, relativeGutterWidth, + strictBoundsChecking, + setDrag, + drag, + setPrevCoords, + prevCoords, + onMouseDown, + onMouseUp, + onMouseDrag, ]); return ; diff --git a/src/components/UserAddModal.tsx b/src/components/UserAddModal.tsx new file mode 100644 index 0000000..ab10f49 --- /dev/null +++ b/src/components/UserAddModal.tsx @@ -0,0 +1,104 @@ +import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; +import { newUninitializedUser, User } from '@luna/contexts/api/auth/types'; +import { CreateOrUpdateUserPayload } from '@luna/contexts/api/auth/types/CreateOrUpdateUserPayload'; +import { + Button, + Checkbox, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@nextui-org/react'; +import { useCallback, useContext, useEffect, useState } from 'react'; + +export interface UserAddModalProps { + isOpen: boolean; + setOpen: (show: boolean) => void; +} + +export function UserAddModal({ isOpen, setOpen }: UserAddModalProps) { + const [user, setUser] = useState(newUninitializedUser()); + const [password, setPassword] = useState(''); + + // initialize/reset modal state + useEffect(() => { + if (!isOpen) return; + setUser(newUninitializedUser()); + setPassword(''); + }, [isOpen]); + + const auth = useContext(AuthContext); + + const addUser = useCallback(async () => { + const payload: CreateOrUpdateUserPayload = { + username: user.username, + password, + email: user.email, + permanent_api_token: user.permanentApiToken, + }; + const result = await auth.createUser(payload); + if (result.ok) { + console.log('added user:', payload); + } else { + console.log('failed to add user:', result.error); + } + // TODO: UI feedback from the request (success, error) + setOpen(false); + }, [setOpen, user, password, auth]); + + return ( + + + {onClose => ( + <> + Add User + + { + if (!user) return; + setUser({ ...user, username }); + }} + /> + + { + if (!user) return; + setUser({ ...user, email }); + }} + /> + { + if (!user) return; + setUser({ + ...user, + permanentApiToken, + }); + }} + > + Permanent API Token + + + + + + + + )} + + + ); +} diff --git a/src/components/UserDeleteModal.tsx b/src/components/UserDeleteModal.tsx new file mode 100644 index 0000000..2c3fb99 --- /dev/null +++ b/src/components/UserDeleteModal.tsx @@ -0,0 +1,116 @@ +import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; +import { newUninitializedUser, User } from '@luna/contexts/api/auth/types'; +import { + Button, + Checkbox, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@nextui-org/react'; +import { useCallback, useContext, useEffect, useState } from 'react'; + +export interface UserDeleteModalProps { + id: number; + isOpen: boolean; + setOpen: (open: boolean) => void; +} + +export function UserDeleteModal({ id, isOpen, setOpen }: UserDeleteModalProps) { + const [user, setUser] = useState(newUninitializedUser()); + + const auth = useContext(AuthContext); + + // initialize modal state + useEffect(() => { + if (!isOpen) return; + + const fetchUser = async () => { + const userResult = await auth.getUserById(id); + if (userResult.ok) { + setUser(userResult.value); + } else { + console.log('Fetching user failed:', userResult.error); + setUser(newUninitializedUser()); + } + }; + fetchUser(); + }, [id, isOpen, auth]); + + const deleteUser = useCallback(() => { + console.log('deleting user with id', id); + // TODO: call DELETE /users/ + // TODO: feedback from the request (success, error) + setOpen(false); + }, [id, setOpen]); + + return ( + + + {onClose => ( + <> + Delete User + + + + { + if (!user) return; + setUser({ ...user, username }); + }} + isDisabled + /> + { + if (!user) return; + setUser({ ...user, email }); + }} + isDisabled + /> + + + + { + if (!user) return; + setUser({ + ...user, + permanentApiToken, + }); + }} + isDisabled + > + Permanent API Token + + + + + + + + )} + + + ); +} diff --git a/src/components/UserDetailsModal.tsx b/src/components/UserDetailsModal.tsx new file mode 100644 index 0000000..e2c4e52 --- /dev/null +++ b/src/components/UserDetailsModal.tsx @@ -0,0 +1,116 @@ +import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; +import { newUninitializedUser, User } from '@luna/contexts/api/auth/types'; +import { + Button, + Checkbox, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Select, + SelectItem, +} from '@nextui-org/react'; +import { useContext, useEffect, useState } from 'react'; + +export interface UserShowModalProps { + id: number; + isOpen: boolean; + setOpen: (open: boolean) => void; +} + +export function UserDetailsModal({ id, isOpen, setOpen }: UserShowModalProps) { + const [user, setUser] = useState(newUninitializedUser()); + + const auth = useContext(AuthContext); + + // initialize modal state + useEffect(() => { + if (!isOpen) return; + const fetchUser = async () => { + const userResult = await auth.getUserById(id); + if (userResult.ok) { + setUser(userResult.value); + } else { + console.log('Failed to fetch user:', userResult.error); + setUser(newUninitializedUser); + } + }; + fetchUser(); + }, [auth, id, isOpen]); + + return ( + + + {onClose => ( + <> + User + + + { + if (!user) return; + setUser({ ...user, username }); + }} + isDisabled + /> + { + if (!user) return; + setUser({ ...user, email }); + }} + isDisabled + /> + + + + + { + if (!user) return; + setUser({ + ...user, + permanentApiToken, + }); + }} + isDisabled + > + Permanent API Token + + {/* TODO: find better component for displaying list of roles */} + + + + + + + + )} + + + ); +} diff --git a/src/components/UserEditModal.tsx b/src/components/UserEditModal.tsx new file mode 100644 index 0000000..df5df65 --- /dev/null +++ b/src/components/UserEditModal.tsx @@ -0,0 +1,132 @@ +import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; + +import { newUninitializedUser, User } from '@luna/contexts/api/auth/types'; +import { CreateOrUpdateUserPayload } from '@luna/contexts/api/auth/types/CreateOrUpdateUserPayload'; +import { + Button, + Checkbox, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@nextui-org/react'; +import { useCallback, useContext, useEffect, useState } from 'react'; + +export interface UserEditModalProps { + id: number; + isOpen: boolean; + setOpen: (open: boolean) => void; +} + +export function UserEditModal({ id, isOpen, setOpen }: UserEditModalProps) { + const [user, setUser] = useState(newUninitializedUser()); + const [password, setPassword] = useState(''); + + const auth = useContext(AuthContext); + + // initialize modal state + useEffect(() => { + if (!isOpen) return; + const fetchUser = async () => { + const userResult = await auth.getUserById(id); + if (userResult.ok) { + setUser(userResult.value); + setPassword(''); + } else { + setUser(newUninitializedUser()); + setPassword(''); + } + }; + fetchUser(); + }, [id, isOpen, auth]); + + const editUser = useCallback(async () => { + const payload: CreateOrUpdateUserPayload = { + username: user.username, + password, + email: user.email, + permanent_api_token: user.permanentApiToken, + }; + const result = await auth.updateUser(id, payload); + if (result.ok) { + console.log('Updated user', id, ':', payload); + } else { + console.log('Update user failed:', result.error); + } + // TODO: feedback from the request (success, error) + setOpen(false); + }, [user, password, auth, id, setOpen]); + + return ( + + + {onClose => ( + <> + Edit User + + + { + if (!user) return; + setUser({ ...user, username }); + }} + /> + + { + if (!user) return; + setUser({ ...user, email }); + }} + /> + + + + { + if (!user) return; + setUser({ + ...user, + permanentApiToken, + }); + }} + > + Permanent API Token + + + + + + + + )} + + + ); +} diff --git a/src/components/UserModal.tsx b/src/components/UserModal.tsx deleted file mode 100644 index 5293e41..0000000 --- a/src/components/UserModal.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { - Button, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, -} from '@nextui-org/react'; - -export interface UserModalProps { - id: number; - action: 'view' | 'add' | 'edit' | 'delete'; - show: boolean; - setShow: (show: boolean) => void; -} -export function UserModal({ id, action, show, setShow }: UserModalProps) { - return ( - setShow(isOpen)}> - - {onClose => ( - <> - - {action.charAt(0).toUpperCase() + action.slice(1)} User - - ID: {id} - - - - - )} - - - ); -} diff --git a/src/components/UserSnippet.tsx b/src/components/UserSnippet.tsx index 4ae68fd..15b0c8f 100644 --- a/src/components/UserSnippet.tsx +++ b/src/components/UserSnippet.tsx @@ -13,7 +13,7 @@ export function UserSnippet({ user, token }: UserSnippetProps) {
: null} + description={} />
diff --git a/src/contexts/api/auth/AuthContext.tsx b/src/contexts/api/auth/AuthContext.tsx index 72348ae..ff233d7 100644 --- a/src/contexts/api/auth/AuthContext.tsx +++ b/src/contexts/api/auth/AuthContext.tsx @@ -1,6 +1,16 @@ import * as convert from '@luna/contexts/api/auth/convert'; import * as generated from '@luna/contexts/api/auth/generated'; -import { Login, Signup, Token, User } from '@luna/contexts/api/auth/types'; +import { + Login, + RegistrationKey, + Role, + Signup, + Token, + User, +} from '@luna/contexts/api/auth/types'; +import { CreateOrUpdateRegistrationKeyPayload } from '@luna/contexts/api/auth/types/CreateOrUpdateRegistrationKeyPayload'; +import { CreateOrUpdateRolePayload } from '@luna/contexts/api/auth/types/CreateOrUpdateRolePayload'; +import { CreateOrUpdateUserPayload } from '@luna/contexts/api/auth/types/CreateOrUpdateUserPayload'; import { useInitRef } from '@luna/hooks/useInitRef'; import { Pagination, slicePage } from '@luna/utils/pagination'; import { errorResult, okResult, Result } from '@luna/utils/result'; @@ -34,9 +44,45 @@ export interface AuthContextValue { /** Fetches all users. */ getAllUsers(pagination?: Pagination): Promise>; - - /** Fetches the public users. */ - getPublicUsers(pagination?: Pagination): Promise>; + /** Gets a user by id */ + getUserById(id: number): Promise>; + /** Creates a new user */ + createUser(payload: CreateOrUpdateUserPayload): Promise>; + /** Updates an existing user */ + updateUser( + id: number, + payload: CreateOrUpdateUserPayload + ): Promise>; + /** Deletes a user */ + deleteUser(id: number): Promise>; + /** Fetches all roles */ + getAllRoles(): Promise>; + /** Gets a role by id */ + getRoleById(id: number): Promise>; + /** Creates a new role */ + createRole(payload: CreateOrUpdateRolePayload): Promise>; + /** Updates an existing role */ + updateRole( + id: number, + payload: CreateOrUpdateRolePayload + ): Promise>; + /** Deletes a role */ + deleteRole(id: number): Promise>; + /** Fetches all registration keys */ + getAllRegistrationKeys(): Promise>; + /** Gets a registration key by id */ + getRegistrationKeyById(id: number): Promise>; + /** Creates a new registration key */ + createRegistrationKey( + payload: CreateOrUpdateRegistrationKeyPayload + ): Promise>; + /** Updates an existing registration key */ + updateRegistrationKey( + id: number, + payload: CreateOrUpdateRegistrationKeyPayload + ): Promise>; + /** Deletes a registration key */ + deleteRegistrationKey(id: number): Promise>; } export const AuthContext = createContext({ @@ -47,7 +93,25 @@ export const AuthContext = createContext({ logIn: async () => errorResult('No auth context for logging in'), logOut: async () => errorResult('No auth context for logging out'), getAllUsers: async () => errorResult('No auth context for fetching users'), - getPublicUsers: async () => errorResult('No auth context for fetching users'), + getUserById: async () => errorResult('No auth context for fetching user'), + createUser: async () => errorResult('No auth context for creating user'), + updateUser: async () => errorResult('No auth context for updating user'), + deleteUser: async () => errorResult('No auth context for deleting user'), + getAllRoles: async () => errorResult('No auth context for fetching roles'), + getRoleById: async () => errorResult('No auth context for fetching role'), + createRole: async () => errorResult('No auth context for creating role'), + updateRole: async () => errorResult('No auth context for updating role'), + deleteRole: async () => errorResult('No auth context for deleting role'), + getAllRegistrationKeys: async () => + errorResult('No auth context for fetching registration keys'), + getRegistrationKeyById: async () => + errorResult('No auth context for fetching registration key'), + createRegistrationKey: async () => + errorResult('No auth context for creating registration key'), + updateRegistrationKey: async () => + errorResult('No auth context for updating registration key'), + deleteRegistrationKey: async () => + errorResult('No auth context for deleting registration key'), }); interface AuthContextProviderProps { @@ -159,9 +223,184 @@ export function AuthContextProvider({ children }: AuthContextProviderProps) { } }, - async getPublicUsers(pagination) { - // TODO: We currently don't have a concept of public users (Heimdall) - return this.getAllUsers(pagination); + async getUserById(id: number) { + try { + const apiUserResponse = await apiRef.current.users.getUserByName(id); + return okResult(convert.userFromApi(apiUserResponse.data)); + } catch (error) { + return errorResult( + `Fetching user with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async createUser(payload: CreateOrUpdateUserPayload) { + try { + await apiRef.current.users.usersCreate( + convert.createOrUpdateUserPayloadToApi(payload) + ); + return okResult(undefined); + } catch (error) { + return errorResult( + `Creating user failed: ${await formatError(error)}` + ); + } + }, + + async updateUser(id: number, payload: CreateOrUpdateUserPayload) { + try { + await apiRef.current.users.usersUpdate( + id, + convert.createOrUpdateUserPayloadToApi(payload) + ); + return okResult(undefined); + } catch (error) { + return errorResult( + `Updating user with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async deleteUser(id: number) { + try { + await apiRef.current.users.usersDelete(id); + return okResult(undefined); + } catch (error) { + return errorResult( + `Deleting user with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async getAllRoles() { + try { + const apiRolesResponse = await apiRef.current.roles.rolesList(); + let apiRoles: generated.Role[] = apiRolesResponse.data; + return okResult(apiRoles.map(convert.roleFromApi)); + } catch (error) { + return errorResult( + `Fetching all roles failed: ${await formatError(error)}` + ); + } + }, + + async getRoleById(id: number) { + try { + const apiRoleResponse = await apiRef.current.roles.rolesDetail(id); + return okResult(convert.roleFromApi(apiRoleResponse.data)); + } catch (error) { + return errorResult( + `Fetching role with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async createRole(payload: CreateOrUpdateRolePayload) { + try { + await apiRef.current.roles.rolesCreate( + convert.createOrUpdateRolePayloadToApi(payload) + ); + return okResult(undefined); + } catch (error) { + return errorResult( + `Creating role failed: ${await formatError(error)}` + ); + } + }, + + async updateRole(id: number, payload: CreateOrUpdateRolePayload) { + try { + await apiRef.current.roles.rolesUpdate( + id, + convert.createOrUpdateRolePayloadToApi(payload) + ); + return okResult(undefined); + } catch (error) { + return errorResult( + `Updating role with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async deleteRole(id: number) { + try { + await apiRef.current.roles.rolesDelete(id); + return okResult(undefined); + } catch (error) { + return errorResult( + `Deleting role with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async getAllRegistrationKeys() { + try { + const apiRegKeysResponse = + await apiRef.current.registrationKeys.registrationKeysList(); + let apiRegKeys: generated.Role[] = apiRegKeysResponse.data; + return okResult(apiRegKeys.map(convert.registrationKeyFromApi)); + } catch (error) { + return errorResult( + `Fetching all registration keys failed: ${await formatError(error)}` + ); + } + }, + + async getRegistrationKeyById(id: number) { + try { + const apiRegKeyResponse = + await apiRef.current.registrationKeys.registrationKeysDetail(id); + return okResult( + convert.registrationKeyFromApi(apiRegKeyResponse.data) + ); + } catch (error) { + return errorResult( + `Fetching registration key with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async createRegistrationKey( + payload: CreateOrUpdateRegistrationKeyPayload + ) { + try { + await apiRef.current.registrationKeys.registrationKeysCreate( + convert.createOrUpdateRegistrationKeyPayloadToApi(payload) + ); + return okResult(undefined); + } catch (error) { + return errorResult( + `Creating registration key failed: ${await formatError(error)}` + ); + } + }, + + async updateRegistrationKey( + id: number, + payload: CreateOrUpdateRegistrationKeyPayload + ) { + try { + await apiRef.current.registrationKeys.registrationKeysUpdate( + id, + convert.createOrUpdateRegistrationKeyPayloadToApi(payload) + ); + return okResult(undefined); + } catch (error) { + return errorResult( + `Updating registration key with id ${id} failed: ${await formatError(error)}` + ); + } + }, + + async deleteRegistrationKey(id: number) { + try { + await apiRef.current.registrationKeys.registrationKeysDelete(id); + return okResult(undefined); + } catch (error) { + return errorResult( + `Deleting registration key with id ${id} failed: ${await formatError(error)}` + ); + } }, }), [apiRef, isInitialized, token, user] diff --git a/src/contexts/api/auth/convert.ts b/src/contexts/api/auth/convert.ts index 9e69128..85d87da 100644 --- a/src/contexts/api/auth/convert.ts +++ b/src/contexts/api/auth/convert.ts @@ -5,7 +5,11 @@ import { Token, User, RegistrationKey, + Role, } from '@luna/contexts/api/auth/types'; +import { CreateOrUpdateRegistrationKeyPayload } from '@luna/contexts/api/auth/types/CreateOrUpdateRegistrationKeyPayload'; +import { CreateOrUpdateRolePayload } from '@luna/contexts/api/auth/types/CreateOrUpdateRolePayload'; +import { CreateOrUpdateUserPayload } from '@luna/contexts/api/auth/types/CreateOrUpdateUserPayload'; export function loginToApi(login?: Login): generated.LoginPayload { return { @@ -26,20 +30,22 @@ export function signupToApi(signup: Signup): generated.RegisterPayload { export function tokenFromApi(apiToken: generated.APIToken): Token { return { value: apiToken.api_token!, - expiresAt: apiToken.expires_at ? new Date(apiToken.expires_at) : undefined, + expiresAt: new Date(apiToken.expires_at!), + username: apiToken.username!, + roles: apiToken.roles!, }; } export function userFromApi(apiUser: generated.User): User { return { - id: apiUser.id, + id: apiUser.id!, username: apiUser.username!, - email: apiUser.email, - roles: undefined, // TODO: change role to roles - createdAt: apiUser.created_at ? new Date(apiUser.created_at) : undefined, - updatedAt: apiUser.updated_at ? new Date(apiUser.updated_at) : undefined, - lastSeen: apiUser.last_login ? new Date(apiUser.last_login) : undefined, - permanentApiToken: apiUser.permanent_api_token, + email: apiUser.email!, + roles: apiUser.roles!.map(apiRole => roleFromApi(apiRole)), // TODO: re-run code generator and use roles + createdAt: new Date(apiUser.created_at!), + updatedAt: new Date(apiUser.updated_at!), + lastSeen: new Date(apiUser.last_login!), + permanentApiToken: apiUser.permanent_api_token!, registrationKey: apiUser.registration_key ? registrationKeyFromApi(apiUser.registration_key) : undefined, @@ -50,18 +56,51 @@ export function registrationKeyFromApi( registrationKey: generated.RegistrationKey ): RegistrationKey { return { - id: registrationKey.id ?? 0, - key: registrationKey.key ?? '', - description: registrationKey.description, - createdAt: registrationKey.created_at - ? new Date(registrationKey.created_at) - : undefined, - updatedAt: registrationKey.updated_at - ? new Date(registrationKey.updated_at) - : undefined, - expiresAt: registrationKey.expires_at - ? new Date(registrationKey.expires_at) - : undefined, - permanent: registrationKey.permanent, + id: registrationKey.id!, + key: registrationKey.key!, + description: registrationKey.description!, + createdAt: new Date(registrationKey.created_at!), + updatedAt: new Date(registrationKey.updated_at!), + expiresAt: new Date(registrationKey.expires_at!), + permanent: registrationKey.permanent!, + }; +} + +export function roleFromApi(role: generated.Role): Role { + return { + id: role.id!, + name: role.name!, + createdAt: new Date(role.created_at!), + updatedAt: new Date(role.updated_at!), + }; +} + +export function createOrUpdateUserPayloadToApi( + payload: CreateOrUpdateUserPayload +): generated.CreateOrUpdateUserPayload { + return { + username: payload.username, + password: payload.password, + email: payload.email, + permanent_api_token: payload.permanent_api_token, + }; +} + +export function createOrUpdateRolePayloadToApi( + payload: CreateOrUpdateRolePayload +): generated.CreateOrUpdateRolePayload { + return { + name: payload.name, + }; +} + +export function createOrUpdateRegistrationKeyPayloadToApi( + payload: CreateOrUpdateRegistrationKeyPayload +): generated.CreateRegistrationKeyPayload { + return { + key: payload.key, + description: payload.description, + expires_at: payload.expires_at.toISOString(), + permanent: payload.permanent, }; } diff --git a/src/contexts/api/auth/generated.ts b/src/contexts/api/auth/generated.ts index ba701e8..20e2401 100644 --- a/src/contexts/api/auth/generated.ts +++ b/src/contexts/api/auth/generated.ts @@ -106,6 +106,8 @@ export interface User { updated_at?: string; /** must be unique */ username?: string; + /** list of roles */ + roles?: Role[]; } export type QueryParamsType = Record; @@ -505,7 +507,7 @@ export class Api extends HttpClient - this.request({ + this.request({ path: `/roles`, method: 'GET', query: query, diff --git a/src/contexts/api/auth/types/CreateOrUpdateRegistrationKeyPayload.ts b/src/contexts/api/auth/types/CreateOrUpdateRegistrationKeyPayload.ts new file mode 100644 index 0000000..2971102 --- /dev/null +++ b/src/contexts/api/auth/types/CreateOrUpdateRegistrationKeyPayload.ts @@ -0,0 +1,6 @@ +export interface CreateOrUpdateRegistrationKeyPayload { + key: string; + description: string; + expires_at: Date; + permanent: boolean; +} diff --git a/src/contexts/api/auth/types/CreateOrUpdateRolePayload.ts b/src/contexts/api/auth/types/CreateOrUpdateRolePayload.ts new file mode 100644 index 0000000..6dcd8eb --- /dev/null +++ b/src/contexts/api/auth/types/CreateOrUpdateRolePayload.ts @@ -0,0 +1,3 @@ +export interface CreateOrUpdateRolePayload { + name: string; +} diff --git a/src/contexts/api/auth/types/CreateOrUpdateUserPayload.ts b/src/contexts/api/auth/types/CreateOrUpdateUserPayload.ts new file mode 100644 index 0000000..e7c5156 --- /dev/null +++ b/src/contexts/api/auth/types/CreateOrUpdateUserPayload.ts @@ -0,0 +1,6 @@ +export interface CreateOrUpdateUserPayload { + username: string; + password: string; + email: string; + permanent_api_token: boolean; +} diff --git a/src/contexts/api/auth/types/RegistrationKey.ts b/src/contexts/api/auth/types/RegistrationKey.ts index aa96948..cf094ab 100644 --- a/src/contexts/api/auth/types/RegistrationKey.ts +++ b/src/contexts/api/auth/types/RegistrationKey.ts @@ -1,9 +1,9 @@ export interface RegistrationKey { id: number; key: string; - description?: string; - createdAt?: Date; - updatedAt?: Date; - expiresAt?: Date; - permanent?: boolean; + description: string; + createdAt: Date; + updatedAt: Date; + expiresAt: Date; + permanent: boolean; } diff --git a/src/contexts/api/auth/types/Role.ts b/src/contexts/api/auth/types/Role.ts index 6bff4ca..f20c0c6 100644 --- a/src/contexts/api/auth/types/Role.ts +++ b/src/contexts/api/auth/types/Role.ts @@ -1,6 +1,6 @@ export interface Role { id: number; name: string; - createdAt?: Date; - updatedAt?: Date; + createdAt: Date; + updatedAt: Date; } diff --git a/src/contexts/api/auth/types/Token.ts b/src/contexts/api/auth/types/Token.ts index ab3eb3f..8e43eca 100644 --- a/src/contexts/api/auth/types/Token.ts +++ b/src/contexts/api/auth/types/Token.ts @@ -1,8 +1,6 @@ -import { Role } from '@luna/contexts/api/auth/types/Role'; - export interface Token { value: string; - expiresAt?: Date; - username?: string; - roles?: Role[]; + expiresAt: Date; + username: string; + roles: string[]; } diff --git a/src/contexts/api/auth/types/User.ts b/src/contexts/api/auth/types/User.ts index 5d5c8b8..3efd6be 100644 --- a/src/contexts/api/auth/types/User.ts +++ b/src/contexts/api/auth/types/User.ts @@ -2,13 +2,35 @@ import { Role } from '@luna/contexts/api/auth/types'; import { RegistrationKey } from '@luna/contexts/api/auth/types/RegistrationKey'; export interface User { - id?: number; + id: number; username: string; - email?: string; - roles?: Role[]; - createdAt?: Date; - updatedAt?: Date; - lastSeen?: Date; - permanentApiToken?: boolean; + email: string; + roles: Role[]; + createdAt: Date; + updatedAt: Date; + lastSeen: Date; + permanentApiToken: boolean; registrationKey?: RegistrationKey; } + +export function newUninitializedUser(): User { + return { + id: 0, + username: '', + email: '', + roles: [], + createdAt: new Date(0), + updatedAt: new Date(0), + lastSeen: new Date(0), + permanentApiToken: false, + registrationKey: { + id: 0, + key: '', + description: '', + createdAt: new Date(0), + updatedAt: new Date(0), + expiresAt: new Date(0), + permanent: false, + }, + }; +} diff --git a/src/contexts/api/model/ModelContext.tsx b/src/contexts/api/model/ModelContext.tsx index 00e7c45..4809488 100644 --- a/src/contexts/api/model/ModelContext.tsx +++ b/src/contexts/api/model/ModelContext.tsx @@ -103,7 +103,7 @@ export function ModelContextProvider({ children }: ModelContextProviderProps) { async function* () { if (!isLoggedIn || !client) return; try { - const users = getOrThrow(await auth.getPublicUsers()); + const users = getOrThrow(await auth.getAllUsers()); // Make sure that every user has at least a black frame for (const { username } of users) { yield { username, frame: new Uint8Array(LIGHTHOUSE_FRAME_BYTES) }; @@ -151,7 +151,7 @@ export function ModelContextProvider({ children }: ModelContextProviderProps) { users, async getLaserMetrics() { const message = await client?.getLaserMetrics(); - if (!message) { + if (!message || message.RNUM >= 400) { return errorResult('Model server provided no laser metrics'); } return okResult(message.PAYL); diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index d5c7384..4c9a02a 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -1,6 +1,8 @@ import { AdminView } from '@luna/screens/home/admin/AdminView'; import { MonitorView } from '@luna/screens/home/admin/MonitorView'; +import { RegistrationKeysView } from '@luna/screens/home/admin/RegistrationKeysView'; import { ResourcesView } from '@luna/screens/home/admin/ResourcesView'; +import { RolesView } from '@luna/screens/home/admin/RolesView'; import { SettingsView } from '@luna/screens/home/admin/SettingsView'; import { UsersView } from '@luna/screens/home/admin/UsersView'; import { RouteObject } from 'react-router-dom'; @@ -17,7 +19,7 @@ export const adminRoute: RouteObject = { element: , }, { - path: 'monitor', + path: 'monitoring', element: , children: [], }, @@ -25,15 +27,14 @@ export const adminRoute: RouteObject = { path: 'users', element: , }, - // TODO: - // { - // path: 'roles', - // element: , - // }, - // { - // path: 'registration-keys', - // element: , - // }, + { + path: 'roles', + element: , + }, + { + path: 'registration-keys', + element: , + }, { path: 'settings', element: , diff --git a/src/screens/home/admin/MonitorView.tsx b/src/screens/home/admin/MonitorView.tsx index 8c618d8..bc365f0 100644 --- a/src/screens/home/admin/MonitorView.tsx +++ b/src/screens/home/admin/MonitorView.tsx @@ -1,10 +1,29 @@ -import { DISPLAY_ASPECT_RATIO, Display } from '@luna/components/Display'; +import { + DISPLAY_ASPECT_RATIO, + Display, + MousePos, +} from '@luna/components/Display'; +import { ModelContext } from '@luna/contexts/api/model/ModelContext'; import { Breakpoint, useBreakpoint } from '@luna/hooks/useBreakpoint'; import { useEventListener } from '@luna/hooks/useEventListener'; import { HomeContent } from '@luna/screens/home/HomeContent'; import { throttle } from '@luna/utils/schedule'; -import { LIGHTHOUSE_FRAME_BYTES } from 'nighthouse/browser'; -import { useMemo, useRef, useState } from 'react'; +import { Button, Card, CardBody, CardHeader, Chip } from '@nextui-org/react'; +import { LIGHTHOUSE_COLS, LIGHTHOUSE_FRAME_BYTES } from 'nighthouse/browser'; +// import { +// LaserMetrics, +// RoomMetrics, +// RoomV2Metrics, +// } from 'nighthouse/out/common/protocol/metrics'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +// import testMetrics from './statusLamps.json'; // TODO: remove testMetrics export function MonitorView() { const [maxSize, setMaxSize] = useState({ width: 0, height: 0 }); @@ -34,26 +53,251 @@ export function MonitorView() { ? maxSize.width : maxSize.height * DISPLAY_ASPECT_RATIO; - // TODO: somehow get the huge JSON object from LaSer streamed through Beacon into here - // then set the color of a pixel according to the state of the lamp/network-controller - // e.g. green: ok, yellow: lamp missing, red: room missing (or whatever) - // TODO: add an overlay to show all metrics when hovering/clicking a window/room - // maybe also show room boundaries e.g. alternating color shades - const frame = new Uint8Array(LIGHTHOUSE_FRAME_BYTES); - for (let i = 0; i < frame.length; i++) { - frame[i] = 255; - } + const model = useContext(ModelContext); + const [metrics, setMetrics] = useState(null); // TODO: change when LaserMetrics includes room + + const [selectedWindow, setSelectedWindow] = useState(0); + + const getLatestMetrics = useCallback(async () => { + // setMetrics(testMetrics); // TODO: change back from test data to fetched data + const m = await model.getLaserMetrics(); // TODO: stream metrics instead + if (m.ok) { + setMetrics(m.value); + } + }, [model]); + + // get the metrics on load + useEffect(() => { + getLatestMetrics(); + }, [getLatestMetrics]); + + // fill the frame with colors according to the metrics data + const frame = useMemo(() => { + const frame = new Uint8Array(LIGHTHOUSE_FRAME_BYTES); + if (!metrics || !metrics.rooms) { + return frame; + } + // alternate between light and dark color to visualize room borders + let parity = false; + let i = 0; + for (const room of metrics!.rooms!) { + if (room.api_version !== 2) continue; + const endIdx = i + 3 * room.lamp_metrics.length; + // controller works? + if (room.controller_metrics.responding) { + let lampIdx = 0; + for (; i < endIdx; i += 3) { + // lamp works? + if (room.lamp_metrics[lampIdx].responding) { + frame[i + 1] = parity ? 255 : 128; // green + } else { + // lamp down -> magenta + frame[i] = parity ? 255 : 128; + frame[i + 2] = parity ? 255 : 128; + } + lampIdx++; + } + } else { + // controller down + for (; i < endIdx; i += 3) { + frame[i] = parity ? 255 : 128; // red + } + } + parity = !parity; + } + // show the selected window in white + if (selectedWindow != null) { + frame[selectedWindow * 3] = 255; + frame[selectedWindow * 3 + 1] = 255; + frame[selectedWindow * 3 + 2] = 255; + } + return frame; + }, [metrics, selectedWindow]); + + // search for the correct room metrics from a single index into the lamp array + const roomMetricsFromIndex = useCallback( + (lampIdx: number) => { + if (!metrics) return null; + let currIdx = 0; + for (const room of metrics.rooms) { + if ( + lampIdx >= currIdx && + lampIdx < currIdx + room.lamp_metrics.length + ) { + return room; + } + currIdx += room.lamp_metrics.length; + } + return null; + }, + [metrics] + ); + + // set the selected window index on click + const onMouseDown = useCallback((p: MousePos) => { + const lampIdx = p.y * LIGHTHOUSE_COLS + p.x; + setSelectedWindow(lampIdx); + }, []); + + // get the selected rooms metrics for rendering + const selectedRoomMetrics = useMemo( + () => roomMetricsFromIndex(selectedWindow), + [roomMetricsFromIndex, selectedWindow] + ); + + // TODO: more appealing UI (maybe tables, inputs or custom stuff?) return ( - +
- +
+ <> + {/* TODO: auto-refresh (polling) or streaming metrics */} + + + {selectedRoomMetrics ? ( + <> + + Room {selectedRoomMetrics.room} + + +
API-Version: {selectedRoomMetrics.api_version}
+
+ Responding: + {selectedRoomMetrics.controller_metrics.responding ? ( + + true + + ) : ( + + false + + )} +
+
+ Ping Latency: + {selectedRoomMetrics.controller_metrics.ping_latency_ms}ms +
+
+ Firmware-Version: + {selectedRoomMetrics.controller_metrics.firmware_version} +
+
+ Uptime: {selectedRoomMetrics.controller_metrics.uptime}s +
+
+ Frames received (total): + {selectedRoomMetrics.controller_metrics.frames} +
+
+ Current frames per second (FPS): + {selectedRoomMetrics.controller_metrics.fps} +
+
+ Core temperature (not very accurate): + {selectedRoomMetrics.controller_metrics.core_temperature} + °C +
+
+ Board temperature (accurate): + {selectedRoomMetrics.controller_metrics.board_temperature} + °C +
+
+ Shunt voltage: + {selectedRoomMetrics.controller_metrics.shunt_voltage}V +
+
+ Voltage: {selectedRoomMetrics.controller_metrics.voltage}V +
+
+ Power: {selectedRoomMetrics.controller_metrics.power}W +
+
+ Current: {selectedRoomMetrics.controller_metrics.current}A +
+
+ Number of lamps responding/connected:{' '} + {selectedRoomMetrics.lamp_metrics.reduce( + (a: number, v: any) => a + (v.responding ? 1 : 0), + 0 + )} + /{selectedRoomMetrics.lamp_metrics.length} +
+
+ + ) : ( + <> + )} +
+ + {selectedRoomMetrics ? ( + <> + + Lamps: + + + {selectedRoomMetrics.lamp_metrics.map( + (lamp: any, idx: number) => ( + <> +
+
+ Lamp {idx + 1}: +
+ Responding:{' '} + {lamp.responding ? ( + + true + + ) : ( + + false + + )} +
+
Firmware-Version: {lamp.firmware_version}
+
Uptime (not very accurate): {lamp.uptime}s
+
Timeout: {lamp.timeout}s
+
+ Temperature (not very accurate): {lamp.temperature} + °C +
+
+ Fuse tripped?{' '} + {lamp.fuse_tripped ? ( + + Yes + + ) : ( + + No + + )} +
+
Flashing status: {lamp.flashing_status}
+
+ + ) + )} +
+ + ) : ( + <> + )} +
+
); diff --git a/src/screens/home/admin/RegistrationKeysView.tsx b/src/screens/home/admin/RegistrationKeysView.tsx new file mode 100644 index 0000000..8fd80c7 --- /dev/null +++ b/src/screens/home/admin/RegistrationKeysView.tsx @@ -0,0 +1,162 @@ +// TODO: enable linter when done +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { RegistrationKey } from '@luna/contexts/api/auth/types'; +import { SearchBar } from '@luna/components/SearchBar'; +import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; +import { HomeContent } from '@luna/screens/home/HomeContent'; +import { getOrThrow } from '@luna/utils/result'; +import { + Button, + Chip, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, + Tooltip, +} from '@nextui-org/react'; +import { useAsyncList } from '@react-stately/data'; +import { IconEye, IconPencil, IconPlus, IconTrash } from '@tabler/icons-react'; +import { useContext, useState } from 'react'; + +export function RegistrationKeysView() { + const auth = useContext(AuthContext); + + const [isLoading, setLoading] = useState(false); + + const keys = useAsyncList({ + initialSortDescriptor: { + column: 'id', + direction: 'ascending', + }, + async load({ cursor, sortDescriptor, filterText }) { + try { + if (cursor !== undefined) { + setLoading(false); + } + let items = getOrThrow(await auth.getAllRegistrationKeys()); + return { items }; + } catch (error) { + console.error( + `Could not fetch registration keys for registration keys view: ${error}` + ); + return { items: [] }; + } + }, + // TODO: correct sorting + }); + + const [showKeyAddModal, setShowKeyAddModal] = useState(false); + const [showKeyEditModal, setShowKeyEditModal] = useState(false); + const [showKeyDetailsModal, setShowKeyDetailsModal] = useState(false); + const [showKeyDeleteModal, setShowKeyDeleteModal] = useState(false); + const [keyId, setKeyId] = useState(0); + + return ( + // TODO: Lazy rendering + + + + + + + } + > + + + + ID + + + Key + + + Description + + + Created At + + + Updated At + + + Expires At + + + Permanent + + Actions + + + {key => ( + + {key.id} + {key.key} + {key.description} + {key.createdAt.toLocaleString()} + {key.updatedAt.toLocaleString()} + {key.expiresAt.toLocaleString()} + + {key.permanent ? ( + + true + + ) : ( + + false + + )} + + +
+ + { + setKeyId(key.id); + setShowKeyDetailsModal(true); + }} + /> + + + { + setKeyId(key.id); + setShowKeyEditModal(true); + }} + /> + + + { + setKeyId(key.id); + setShowKeyDeleteModal(true); + }} + /> + +
+
+
+ )} +
+
+
+ ); +} diff --git a/src/screens/home/admin/RolesView.tsx b/src/screens/home/admin/RolesView.tsx new file mode 100644 index 0000000..e4fe595 --- /dev/null +++ b/src/screens/home/admin/RolesView.tsx @@ -0,0 +1,144 @@ +// TODO: enable linter when done +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Role } from '@luna/contexts/api/auth/types'; +import { SearchBar } from '@luna/components/SearchBar'; +import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; +import { HomeContent } from '@luna/screens/home/HomeContent'; +import { getOrThrow } from '@luna/utils/result'; +import { + Button, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, + Tooltip, +} from '@nextui-org/react'; +import { useAsyncList } from '@react-stately/data'; +import { + IconCategoryPlus, + IconEye, + IconPencil, + IconTrash, +} from '@tabler/icons-react'; +import { useContext, useState } from 'react'; + +export function RolesView() { + const auth = useContext(AuthContext); + + const [isLoading, setLoading] = useState(false); + + const roles = useAsyncList({ + initialSortDescriptor: { + column: 'id', + direction: 'ascending', + }, + async load({ cursor, sortDescriptor, filterText }) { + try { + if (cursor !== undefined) { + setLoading(false); + } + let items = getOrThrow(await auth.getAllRoles()); + return { items }; + } catch (error) { + console.error(`Could not fetch roles for roles view: ${error}`); + return { items: [] }; + } + }, + // TODO: correct sorting + }); + + // TODO: role add/details/edit/delete modals + + const [showRoleAddModal, setShowRoleAddModal] = useState(false); + const [showRoleEditModal, setShowRoleEditModal] = useState(false); + const [showRoleDetailsModal, setShowRoleDetailsModal] = useState(false); + const [showRoleDeleteModal, setShowRoleDeleteModal] = useState(false); + const [roleId, setRoleId] = useState(0); + + return ( + // TODO: Lazy rendering + + + + + + + } + > + + + + ID + + + Name + + + Created At + + + Updated At + + Actions + + + {role => ( + + {role.id} + {role.name} + {role.createdAt.toLocaleString()} + {role.updatedAt.toLocaleString()} + +
+ + { + setRoleId(role.id); + setShowRoleDetailsModal(true); + }} + /> + + + { + setRoleId(role.id); + setShowRoleEditModal(true); + }} + /> + + + { + setRoleId(role.id); + setShowRoleDeleteModal(true); + }} + /> + +
+
+
+ )} +
+
+
+ ); +} diff --git a/src/screens/home/admin/UsersView.tsx b/src/screens/home/admin/UsersView.tsx index 9cfb8eb..48a3054 100644 --- a/src/screens/home/admin/UsersView.tsx +++ b/src/screens/home/admin/UsersView.tsx @@ -1,6 +1,9 @@ import { User } from '@luna/contexts/api/auth/types'; +import { UserAddModal } from '@luna/components/UserAddModal'; +import { UserDeleteModal } from '@luna/components/UserDeleteModal'; +import { UserDetailsModal } from '@luna/components/UserDetailsModal'; +import { UserEditModal } from '@luna/components/UserEditModal'; import { SearchBar } from '@luna/components/SearchBar'; -import { UserModal } from '@luna/components/UserModal'; import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; import { HomeContent } from '@luna/screens/home/HomeContent'; import { getOrThrow } from '@luna/utils/result'; @@ -73,10 +76,11 @@ export function UsersView() { } }, [users, hasMore, needsMore]); - const [userModal, setUserModal] = useState<{ - id: number; - action: 'add' | 'view' | 'edit' | 'delete'; - } | null>(null); + const [showUserAddModal, setShowUserAddModal] = useState(false); + const [showUserEditModal, setShowUserEditModal] = useState(false); + const [showUserDetailsModal, setShowUserDetailsModal] = useState(false); + const [showUserDeleteModal, setShowUserDeleteModal] = useState(false); + const [userId, setUserId] = useState(0); return ( // TODO: Lazy rendering @@ -89,23 +93,29 @@ export function UsersView() { setQuery={users.setFilterText} /> - } > - !show && setUserModal(null)} + + + + - E-Mail - {/* TODO: move to details modal: - Roles - */} Created At @@ -145,9 +152,6 @@ export function UsersView() { Permanent API-Token - {/* TODO: move to details modal: - Registration-Key - */} Actions @@ -156,10 +160,9 @@ export function UsersView() { {user.id} {user.username} {user.email} - {/* TODO: move to details modal: {user.roles?.map(role => role.name)} */} - {user.createdAt?.toLocaleString()} - {user.updatedAt?.toLocaleString()} - {user.lastSeen?.toLocaleString()} + {user.createdAt.toLocaleString()} + {user.updatedAt.toLocaleString()} + {user.lastSeen.toLocaleString()} {user.permanentApiToken ? ( @@ -171,14 +174,14 @@ export function UsersView() { )} - {/* TODO: move to details modal: {user.registrationKey?.key} */}
{ - setUserModal({ id: user.id ?? 0, action: 'view' }); + setUserId(user.id); + setShowUserDetailsModal(true); }} /> @@ -186,7 +189,8 @@ export function UsersView() { { - setUserModal({ id: user.id ?? 0, action: 'edit' }); + setUserId(user.id); + setShowUserEditModal(true); }} /> @@ -194,7 +198,8 @@ export function UsersView() { { - setUserModal({ id: user.id ?? 0, action: 'delete' }); + setUserId(user.id); + setShowUserDeleteModal(true); }} /> diff --git a/src/screens/home/displays/DisplayInspector.tsx b/src/screens/home/displays/DisplayInspector.tsx index 7965ce2..bf605ca 100644 --- a/src/screens/home/displays/DisplayInspector.tsx +++ b/src/screens/home/displays/DisplayInspector.tsx @@ -1,7 +1,7 @@ import { AuthContext } from '@luna/contexts/api/auth/AuthContext'; import { DisplayInspectorApiTokenCard } from '@luna/screens/home/displays/DisplayInspectorApiTokenCard'; import { DisplayInspectorInputCard } from '@luna/screens/home/displays/DisplayInspectorInputCard'; -import { DisplayInspectorOptionsCard } from '@luna/screens/home/displays/DisplayInspectorOptionsCard'; +// import { DisplayInspectorOptionsCard } from '@luna/screens/home/displays/DisplayInspectorOptionsCard'; import { useContext } from 'react'; export interface DisplayInspectorProps { @@ -12,11 +12,11 @@ export function DisplayInspector({ username }: DisplayInspectorProps) { const { user: me } = useContext(AuthContext); const isMeOrAdmin = username === me?.username || - me?.roles?.find(role => role.name === 'admin') !== undefined; + me?.roles.find(role => role.name === 'admin') !== undefined; return (
- + {/* */} {isMeOrAdmin ? ( <> diff --git a/src/screens/home/displays/DisplayInspectorInputCard.tsx b/src/screens/home/displays/DisplayInspectorInputCard.tsx index b27344b..d91c4a7 100644 --- a/src/screens/home/displays/DisplayInspectorInputCard.tsx +++ b/src/screens/home/displays/DisplayInspectorInputCard.tsx @@ -6,6 +6,7 @@ export function DisplayInspectorInputCard() { return ( } title="Input">
+ Mouse Keyboard Controller
diff --git a/src/screens/home/sidebar/SidebarRoutes.tsx b/src/screens/home/sidebar/SidebarRoutes.tsx index 13234f6..2d3725b 100644 --- a/src/screens/home/sidebar/SidebarRoutes.tsx +++ b/src/screens/home/sidebar/SidebarRoutes.tsx @@ -3,8 +3,10 @@ import { RouteLink } from '@luna/components/RouteLink'; import { truncate } from '@luna/utils/string'; import { IconBuildingLighthouse, + IconCategory, IconFolder, IconHeartRateMonitor, + IconKey, IconSettings, IconTower, IconUsers, @@ -23,24 +25,38 @@ export const SidebarRoutes = memo( ({ isCompact, searchQuery, user, allUsernames }: SidebarRoutesProps) => { return ( <> - } name="Admin" path="/admin"> - } - name="Resources" - path="/admin/resources" - /> - } - name="Monitor" - path="/admin/monitor" - /> - } name="Users" path="/admin/users" /> - } - name="Settings" - path="/admin/settings" - /> - + {user?.roles.find(role => role.name === 'admin') !== undefined ? ( + } name="Admin" path="/admin"> + } + name="Resources" + path="/admin/resources" + /> + } + name="Monitoring" + path="/admin/monitoring" + /> + } name="Users" path="/admin/users" /> + } + name="Roles" + path="/admin/roles" + /> + } + name="Registration Keys" + path="/admin/registration-keys" + /> + } + name="Settings" + path="/admin/settings" + /> + + ) : ( + <> + )} } name="Displays"