feat: add navigation and user settings
This commit is contained in:
parent
71b9fc22b7
commit
ed8df13b2f
4
www/src/data/constants.js
Normal file
4
www/src/data/constants.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const DEVICES_VIEW = "devices";
|
||||||
|
export const REMOTES_VIEW = "remotes";
|
||||||
|
export const RECORDINGS_VIEW = "recordings";
|
||||||
|
export const SETTINGS_VIEW = "settings";
|
||||||
132
www/src/modals/create-user-modal.jsx
Normal file
132
www/src/modals/create-user-modal.jsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { createEffect, createSignal } from "solid-js";
|
||||||
|
import Modal from "./modal.jsx";
|
||||||
|
import UserService from "../services/user-service.js";
|
||||||
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
|
import ModalHandler from "./modal-handler.js";
|
||||||
|
|
||||||
|
const [users, setUsers] = createSignal([]);
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
const USER_CREATED_EVENT = "success";
|
||||||
|
|
||||||
|
const MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
function CreateUserModal(props) {
|
||||||
|
const [username, setUsername] = createSignal("");
|
||||||
|
const [isUsernameValid, setUsernameValid] = createSignal(true);
|
||||||
|
const [password, setPassword] = createSignal("");
|
||||||
|
const [isPasswordValid, setPasswordValid] = createSignal(true);
|
||||||
|
const [isAdmin, setAdmin] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
|
||||||
|
createEffect(() => setUsernameValid(checkIfUsernameValid(username())));
|
||||||
|
createEffect(() => setPasswordValid(checkIfPasswordValid(password())));
|
||||||
|
|
||||||
|
function checkIfUsernameValid(name) {
|
||||||
|
for (let user of users()) {
|
||||||
|
if (user.getUsername() !== name) continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfPasswordValid(password) {
|
||||||
|
return password.length >= MIN_PASSWORD_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateUser() {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await UserService.createUser({
|
||||||
|
username: username(),
|
||||||
|
password: password(),
|
||||||
|
isAdmin: isAdmin(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUsername("");
|
||||||
|
setPassword("");
|
||||||
|
CreateUserModal.Handler.hide();
|
||||||
|
eventEmitter.dispatchEvent(USER_CREATED_EVENT, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={props.ref}
|
||||||
|
id="createUserModal"
|
||||||
|
modalTitle="New User"
|
||||||
|
centered={true}
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<Show when={error() !== ""}>
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="new_username" class="col-form-label col-sm-2">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<ValidatedTextInput
|
||||||
|
class="col-sm-10"
|
||||||
|
id="new_username"
|
||||||
|
valid={isUsernameValid()}
|
||||||
|
value={username()}
|
||||||
|
onInput={(e) => setUsername(e.target.value)}
|
||||||
|
errorText={"Username already exists"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="user_password" class="col-form-label col-sm-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<ValidatedTextInput
|
||||||
|
class="col-sm-10"
|
||||||
|
id="user_password"
|
||||||
|
type="password"
|
||||||
|
valid={isPasswordValid()}
|
||||||
|
value={password()}
|
||||||
|
onInput={(e) => setPassword(e.target.value)}
|
||||||
|
errorText={"Invalid password"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="is_admin" class="col-form-label col-sm-2">
|
||||||
|
Admin
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10 d-flex align-items-center">
|
||||||
|
<input
|
||||||
|
id="is_admin"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
checked={isAdmin()}
|
||||||
|
onChange={() => setAdmin(!isAdmin())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateUser}
|
||||||
|
class="btn btn-primary"
|
||||||
|
disabled={!isUsernameValid() && !isPasswordValid()}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateUserModal.Handler = new ModalHandler();
|
||||||
|
CreateUserModal.setUsers = setUsers;
|
||||||
|
CreateUserModal.onUserCreated = (callback) =>
|
||||||
|
eventEmitter.on(USER_CREATED_EVENT, callback);
|
||||||
|
|
||||||
|
export default CreateUserModal;
|
||||||
88
www/src/modals/delete-user-modal.jsx
Normal file
88
www/src/modals/delete-user-modal.jsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { createEffect, createSignal } from "solid-js";
|
||||||
|
import Modal from "./modal.jsx";
|
||||||
|
import UserService from "../services/user-service.js";
|
||||||
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
|
import User from "../data/user.js";
|
||||||
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
|
import ModalHandler from "./modal-handler.js";
|
||||||
|
|
||||||
|
const [user, setUser] = createSignal(new User());
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
const USER_DELETED_EVENT = "success";
|
||||||
|
|
||||||
|
function DeleteUserModal(props) {
|
||||||
|
const [username, setUsername] = createSignal("");
|
||||||
|
const [isUsernameValid, setUsernameValid] = createSignal(true);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
|
||||||
|
createEffect(() => setUsernameValid(checkIfUsernameValid(username())));
|
||||||
|
|
||||||
|
function checkIfUsernameValid(name) {
|
||||||
|
return name === user().getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteUser() {
|
||||||
|
try {
|
||||||
|
await UserService.deleteUser(user().getId());
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
DeleteUserModal.Handler.hide();
|
||||||
|
eventEmitter.dispatchEvent(USER_DELETED_EVENT, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={props.ref}
|
||||||
|
id="deleteUserModal"
|
||||||
|
modalTitle="Delete user"
|
||||||
|
centered={true}
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<Show when={error() !== ""}>
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<span>
|
||||||
|
Do you really want to delete the user {user().getUsername()}?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="del_username" class="col-form-label">
|
||||||
|
Type <b>{user().getUsername()}</b> to confirm:
|
||||||
|
</label>
|
||||||
|
<ValidatedTextInput
|
||||||
|
id="del_username"
|
||||||
|
valid={isUsernameValid()}
|
||||||
|
value={username()}
|
||||||
|
onInput={(e) => setUsername(e.target.value)}
|
||||||
|
errorText={"Wrong input"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDeleteUser}
|
||||||
|
class="btn btn-danger"
|
||||||
|
disabled={!isUsernameValid()}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteUserModal.Handler = new ModalHandler();
|
||||||
|
DeleteUserModal.setUser = setUser;
|
||||||
|
DeleteUserModal.onUserDeleted = (callback) =>
|
||||||
|
eventEmitter.on(USER_DELETED_EVENT, callback);
|
||||||
|
|
||||||
|
export default DeleteUserModal;
|
||||||
31
www/src/modals/modal-handler.js
Normal file
31
www/src/modals/modal-handler.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
function ModalHandler() {
|
||||||
|
let _ref;
|
||||||
|
let _modalRef;
|
||||||
|
let _modalId;
|
||||||
|
|
||||||
|
function setRef(ref) {
|
||||||
|
_ref = ref;
|
||||||
|
_modalRef = new bootstrap.Modal(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
_modalRef.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
_modalRef.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setModalId(modalId) {
|
||||||
|
_modalId = modalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setRef,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
setModalId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalHandler;
|
||||||
49
www/src/modals/modal-registry.jsx
Normal file
49
www/src/modals/modal-registry.jsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { onMount } from "solid-js";
|
||||||
|
|
||||||
|
import CreateUserModal from "./create-user-modal.jsx";
|
||||||
|
import DeleteUserModal from "./delete-user-modal.jsx";
|
||||||
|
import UserSettingsModal from "./user-settings-modal.jsx";
|
||||||
|
|
||||||
|
const ModalRegistry = (function () {
|
||||||
|
const modals = [
|
||||||
|
{
|
||||||
|
id: "createUserModal",
|
||||||
|
component: CreateUserModal,
|
||||||
|
ref: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deleteUserModal",
|
||||||
|
component: DeleteUserModal,
|
||||||
|
ref: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userSettingsModal",
|
||||||
|
component: UserSettingsModal,
|
||||||
|
ref: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getModals(props) {
|
||||||
|
return function () {
|
||||||
|
onMount(() => {
|
||||||
|
modals.forEach((modal) => {
|
||||||
|
modal.component.Handler.setRef(modal.ref);
|
||||||
|
modal.component.Handler.setModalId(modal.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<For each={modals}>
|
||||||
|
{(modal) => <modal.component ref={modal.ref} />}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getModals,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default ModalRegistry;
|
||||||
45
www/src/modals/modal.jsx
Normal file
45
www/src/modals/modal.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { mergeProps } from "solid-js";
|
||||||
|
|
||||||
|
function Modal(props) {
|
||||||
|
props = mergeProps(
|
||||||
|
{ size: "", id: "modal", modalTitle: "Modal", centered: false },
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={"modal fade " + props.size}
|
||||||
|
id={props.id}
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby={props.id + "Label"}
|
||||||
|
aria-hidden="true"
|
||||||
|
ref={props.ref}
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-dialog modal-dialog-scrollable modal-fullscreen-md-down modal-lg"
|
||||||
|
classList={{ "modal-dialog-centered": props.centered }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-content"
|
||||||
|
style={props.fullHeight ? "height: 100%" : ""}
|
||||||
|
>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id={props.id + "Label"}>
|
||||||
|
{props.modalTitle}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Schliessen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
88
www/src/modals/user-settings-modal.jsx
Normal file
88
www/src/modals/user-settings-modal.jsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
} from "solid-js";
|
||||||
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
|
import User from "../data/user.js";
|
||||||
|
import Modal from "./modal.jsx";
|
||||||
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
|
import ModalHandler from "./modal-handler.js";
|
||||||
|
import UserService from "../services/user-service.js";
|
||||||
|
|
||||||
|
const [user, setUser] = createSignal(new User());
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
const USER_UPDATED_EVENT = "user-updated";
|
||||||
|
|
||||||
|
function UserSettingsModal(props) {
|
||||||
|
const [isAdmin, setAdmin] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
const [userInfo] = createResource(() => UserService.getUserInfo());
|
||||||
|
let canEditAdmin = createMemo(
|
||||||
|
() => userInfo.loading || user().getId() !== userInfo().user_id
|
||||||
|
);
|
||||||
|
createEffect(() => setAdmin(user().isAdmin()));
|
||||||
|
|
||||||
|
async function handleSaveSettings() {
|
||||||
|
try {
|
||||||
|
await UserService.updateUser(user().getId(), { isAdmin: isAdmin() });
|
||||||
|
} catch (error) {
|
||||||
|
setError(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UserSettingsModal.Handler.hide();
|
||||||
|
eventEmitter.dispatchEvent(USER_UPDATED_EVENT, user());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={props.ref}
|
||||||
|
id="userSettingsModal"
|
||||||
|
modalTitle={`Settings for ${user().getUsername()}`}
|
||||||
|
centered={true}
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<Show when={error() !== ""}>
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label for="is_admin" class="col-form-label col-sm-2">
|
||||||
|
Admin
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10 d-flex align-items-center">
|
||||||
|
<input
|
||||||
|
id="is_admin"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
checked={isAdmin()}
|
||||||
|
disabled={!canEditAdmin()}
|
||||||
|
onChange={() => setAdmin(!isAdmin())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveSettings}
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSettingsModal.Handler = new ModalHandler();
|
||||||
|
UserSettingsModal.setUser = setUser;
|
||||||
|
UserSettingsModal.onUserUpdated = (callback) =>
|
||||||
|
eventEmitter.on(USER_UPDATED_EVENT, callback);
|
||||||
|
|
||||||
|
export default UserSettingsModal;
|
||||||
211
www/src/modules/list.jsx
Normal file
211
www/src/modules/list.jsx
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import {
|
||||||
|
Match,
|
||||||
|
Show,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
mergeProps,
|
||||||
|
} from "solid-js";
|
||||||
|
|
||||||
|
function List(props) {
|
||||||
|
props = mergeProps({ items: [], showHeader: true, selectable: true }, props);
|
||||||
|
const [listItems, setListItems] = createSignal([]);
|
||||||
|
const selectedItems = createMemo(() =>
|
||||||
|
listItems()
|
||||||
|
.filter((item) => item.selected())
|
||||||
|
.map((item) => item.item)
|
||||||
|
);
|
||||||
|
const allItemsSelected = createMemo(() =>
|
||||||
|
listItems().every((item) => item.selected())
|
||||||
|
);
|
||||||
|
createEffect(() => {
|
||||||
|
setListItems(
|
||||||
|
props.items.map((item) => {
|
||||||
|
const [selected, setSelected] = createSignal(false);
|
||||||
|
return {
|
||||||
|
selected,
|
||||||
|
setSelected,
|
||||||
|
item: item,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.onListItemsSelect) return;
|
||||||
|
props.onListItemsSelect(selectedItems());
|
||||||
|
});
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (props.onLazyLoad) {
|
||||||
|
props.onLazyLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const [lazyLoadTriggerElement, setLazyLoadTriggerElement] = createSignal();
|
||||||
|
createEffect(() => {
|
||||||
|
observer.observe(lazyLoadTriggerElement());
|
||||||
|
});
|
||||||
|
|
||||||
|
const [scrollTop, setScrollTop] = createSignal(0);
|
||||||
|
const itemsMutationObserver = new MutationObserver(() => {
|
||||||
|
itemsContainerElement().scrollTop = scrollTop();
|
||||||
|
});
|
||||||
|
const [itemsContainerElement, setItemsContainerElement] = createSignal();
|
||||||
|
createEffect(() => {
|
||||||
|
if (!itemsContainerElement()) return;
|
||||||
|
itemsMutationObserver.observe(itemsContainerElement(), { childList: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleListItemClick(item) {
|
||||||
|
if (!props.onListItemClick) return;
|
||||||
|
props.onListItemClick(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleAllItems() {
|
||||||
|
if (allItemsSelected()) {
|
||||||
|
listItems().forEach((item) => item.setSelected(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listItems().forEach((item) => item.setSelected(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeaderRow(columns) {
|
||||||
|
columns = columns.filter((column) => !column.hidden);
|
||||||
|
if (!props.showHeader) return null;
|
||||||
|
return (
|
||||||
|
<div class="list-group-item d-flex p-0">
|
||||||
|
<div class="d-flex flex-fill border-bottom border-2 fw-bold overflow-y-scroll">
|
||||||
|
<Show when={props.selectable}>
|
||||||
|
<div
|
||||||
|
class="d-flex align-items-center justify-content-center border-end py-2 px-3"
|
||||||
|
onClick={() => handleToggleAllItems()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input m-0"
|
||||||
|
type="checkbox"
|
||||||
|
checked={allItemsSelected()}
|
||||||
|
id={""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<For each={columns}>
|
||||||
|
{(column) => (
|
||||||
|
<div
|
||||||
|
class={"d-flex p-2" + (column.width ? "" : " flex-fill")}
|
||||||
|
style={column.width ? `width:${column.width}em` : ""}
|
||||||
|
>
|
||||||
|
<Show when={column.withIcons}>
|
||||||
|
<div class="pe-2" style="color: transparent">
|
||||||
|
<i class="bi-folder2" />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="text-truncate">
|
||||||
|
<span>{column.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderListItem(listItem, columns) {
|
||||||
|
columns = columns.filter((column) => !column.hidden);
|
||||||
|
let item = listItem.item;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="list-group-item list-group-item-action d-flex p-0 overflow-x-hidden border-0 border-bottom border-1"
|
||||||
|
style="max-width: 100%"
|
||||||
|
>
|
||||||
|
<Show when={props.selectable}>
|
||||||
|
<div
|
||||||
|
class="d-flex align-items-center justify-content-center border-end py-2 px-3"
|
||||||
|
onClick={() => listItem.setSelected(!listItem.selected())}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input m-0"
|
||||||
|
type="checkbox"
|
||||||
|
checked={listItem.selected()}
|
||||||
|
id={""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class="d-flex flex-fill overflow-hidden"
|
||||||
|
onClick={() => handleListItemClick(item)}
|
||||||
|
>
|
||||||
|
<For each={columns}>
|
||||||
|
{(column) => (
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
"d-flex p-2 align-items-center" +
|
||||||
|
(column.width ? " flex-shrink-0" : " flex-fill")
|
||||||
|
}
|
||||||
|
style={
|
||||||
|
"overflow: hidden; " +
|
||||||
|
(column.width ? `width:${column.width}em` : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show when={column.withIcons}>
|
||||||
|
<Show when={!item[column.id].icon}>
|
||||||
|
<div class="pe-2" style="color: transparent">
|
||||||
|
<i class="bi-folder2" />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={item[column.id].icon}>
|
||||||
|
<div class="pe-2">
|
||||||
|
<i class={item[column.id].icon} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
<Switch>
|
||||||
|
<Match when={item[column.id].html}>
|
||||||
|
{item[column.id].html}
|
||||||
|
</Match>
|
||||||
|
<Match when={item[column.id].text}>
|
||||||
|
<div class="text-truncate">
|
||||||
|
<span>{item[column.id]?.text}</span>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={props.style}
|
||||||
|
class={
|
||||||
|
"list-group d-flex flex-column overflow-hidden " + (props.class || "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderHeaderRow(props.columns)}
|
||||||
|
<div
|
||||||
|
class="list-group-item p-0 flex-fill overflow-y-scroll"
|
||||||
|
style="min-width: 0"
|
||||||
|
onScroll={(event) => setScrollTop(event.target.scrollTop)}
|
||||||
|
ref={setItemsContainerElement}
|
||||||
|
>
|
||||||
|
<For each={listItems()}>
|
||||||
|
{(listItem) => renderListItem(listItem, props.columns)}
|
||||||
|
</For>
|
||||||
|
<div
|
||||||
|
style="width: 100%; height: 3em;"
|
||||||
|
ref={setLazyLoadTriggerElement}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default List;
|
||||||
127
www/src/modules/settings/change-password.jsx
Normal file
127
www/src/modules/settings/change-password.jsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { createEffect, createSignal } from "solid-js";
|
||||||
|
import UserService from "../../services/user-service";
|
||||||
|
import ValidatedTextInput from "../validated-text-input";
|
||||||
|
|
||||||
|
function ChangePasswordSettings() {
|
||||||
|
const PASSWORD_MIN_LENGTH = 6;
|
||||||
|
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
const [success, setSuccess] = createSignal(false);
|
||||||
|
|
||||||
|
const [currentPassword, setCurrentPassword] = createSignal("");
|
||||||
|
const [newPassword, setNewPassword] = createSignal("");
|
||||||
|
const [newPasswordRepeat, setNewPasswordRepeat] = createSignal("");
|
||||||
|
|
||||||
|
const [isCurrentPasswordValid, setCurrentPasswordValid] = createSignal(true);
|
||||||
|
const [isNewPasswordValid, setNewPasswordValid] = createSignal(true);
|
||||||
|
const [isNewPasswordRepeatValid, setNewPasswordRepeatValid] =
|
||||||
|
createSignal(true);
|
||||||
|
const [isValid, setValid] = createSignal(false);
|
||||||
|
|
||||||
|
const [currentPasswordActive, setCurrentPasswordActive] = createSignal(false);
|
||||||
|
const [newPasswordRepeatActive, setNewPasswordRepeatActive] =
|
||||||
|
createSignal(false);
|
||||||
|
|
||||||
|
createEffect(() =>
|
||||||
|
setValid(
|
||||||
|
isCurrentPasswordValid() &&
|
||||||
|
isNewPasswordValid() &&
|
||||||
|
isNewPasswordRepeatValid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
createEffect(() => setCurrentPasswordValid(currentPassword().length > 0));
|
||||||
|
createEffect(() =>
|
||||||
|
setNewPasswordValid(newPassword().length >= PASSWORD_MIN_LENGTH)
|
||||||
|
);
|
||||||
|
createEffect(() =>
|
||||||
|
setNewPasswordRepeatValid(newPassword() === newPasswordRepeat())
|
||||||
|
);
|
||||||
|
createEffect(() => (error() ? setSuccess(false) : null));
|
||||||
|
createEffect(() =>
|
||||||
|
success() ? setTimeout(() => setSuccess(false), 5000) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleChangePassword() {
|
||||||
|
try {
|
||||||
|
await UserService.changePassword({
|
||||||
|
currentPassword: currentPassword(),
|
||||||
|
newPassword: newPassword(),
|
||||||
|
});
|
||||||
|
setSuccess(true);
|
||||||
|
} catch (error) {
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="border rounded bg-dark p-3">
|
||||||
|
<div
|
||||||
|
class={"alert alert-danger" + (error() ? "" : " d-none")}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={"alert alert-success" + (success() ? "" : " d-none")}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
Changing password successful.
|
||||||
|
</div>
|
||||||
|
<div class="fw-bold mb-3">Change password</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="currentPassword" class="col-sm-2 col-form-label">
|
||||||
|
Current Password
|
||||||
|
</label>
|
||||||
|
<ValidatedTextInput
|
||||||
|
class="col-sm-10"
|
||||||
|
id="currentPassword"
|
||||||
|
type="password"
|
||||||
|
valid={isCurrentPasswordValid()}
|
||||||
|
value={currentPassword()}
|
||||||
|
onInput={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="newPassword" class="col-sm-2 col-form-label">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<ValidatedTextInput
|
||||||
|
class="col-sm-10"
|
||||||
|
id="newPassword"
|
||||||
|
type="password"
|
||||||
|
valid={isNewPasswordValid()}
|
||||||
|
value={newPassword()}
|
||||||
|
onInput={(e) => setNewPassword(e.target.value)}
|
||||||
|
errorText={`Neues Passwort muss mindestens ${PASSWORD_MIN_LENGTH} Zeichen lang
|
||||||
|
sein.`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="newPasswordRepeat" class="col-sm-2 col-form-label">
|
||||||
|
New Passwort repeat
|
||||||
|
</label>
|
||||||
|
<ValidatedTextInput
|
||||||
|
class="col-sm-10"
|
||||||
|
id="newPasswordRepeat"
|
||||||
|
type="password"
|
||||||
|
valid={isNewPasswordRepeatValid()}
|
||||||
|
value={newPasswordRepeat()}
|
||||||
|
onInput={(e) => setNewPasswordRepeat(e.target.value)}
|
||||||
|
errorText={"Passwörter müssen übereinstimmen"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
disabled={!isValid()}
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangePasswordSettings;
|
||||||
80
www/src/modules/settings/users-list.jsx
Normal file
80
www/src/modules/settings/users-list.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { createSignal, onMount } from "solid-js";
|
||||||
|
import List from "../list.jsx";
|
||||||
|
import UserService from "../../services/user-service.js";
|
||||||
|
import CreateUserModal from "../../modals/create-user-modal.jsx";
|
||||||
|
import DeleteUserModal from "../../modals/delete-user-modal.jsx";
|
||||||
|
import UserSettingsModal from "../../modals/user-settings-modal.jsx";
|
||||||
|
|
||||||
|
function UserList() {
|
||||||
|
const [users, setUsers] = createSignal([]);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateUsers();
|
||||||
|
CreateUserModal.onUserCreated(() => updateUsers());
|
||||||
|
UserSettingsModal.onUserUpdated(() => updateUsers());
|
||||||
|
DeleteUserModal.onUserDeleted(() => updateUsers());
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateUsers() {
|
||||||
|
let users = await UserService.getUsers();
|
||||||
|
setUsers(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShowUserSettings(user) {
|
||||||
|
UserSettingsModal.setUser(user);
|
||||||
|
UserSettingsModal.Handler.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateUser() {
|
||||||
|
CreateUserModal.setUsers(users());
|
||||||
|
CreateUserModal.Handler.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteUser(user) {
|
||||||
|
DeleteUserModal.setUser(user);
|
||||||
|
DeleteUserModal.Handler.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="d-flex flex-row justify-content-end">
|
||||||
|
<button class="btn btn-dark me-2 mb-3" onClick={handleCreateUser}>
|
||||||
|
<i class="bi bi-plus me-2"></i>
|
||||||
|
New User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
class="h-100"
|
||||||
|
columns={[
|
||||||
|
{ id: "username", name: "Username" },
|
||||||
|
{ id: "is_admin", name: "Admin", width: 10 },
|
||||||
|
{ id: "options", name: "Options", width: 10 },
|
||||||
|
]}
|
||||||
|
items={users().map((user) => ({
|
||||||
|
username: { text: user.getUsername() },
|
||||||
|
is_admin: { text: user.isAdmin() ? "Yes" : "No" },
|
||||||
|
options: {
|
||||||
|
html: (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary me-2"
|
||||||
|
onClick={() => handleShowUserSettings(user)}
|
||||||
|
>
|
||||||
|
<i class="bi bi-gear-fill"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary me-2"
|
||||||
|
onClick={() => handleDeleteUser(user)}
|
||||||
|
>
|
||||||
|
<i class="bi bi-trash-fill"></i>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserList;
|
||||||
33
www/src/modules/validated-text-input.jsx
Normal file
33
www/src/modules/validated-text-input.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { createSignal, mergeProps } from "solid-js";
|
||||||
|
|
||||||
|
function ValidatedTextInput(props) {
|
||||||
|
props = mergeProps(
|
||||||
|
{ type: "text", valid: true, onInput: () => {}, errorText: "" },
|
||||||
|
props
|
||||||
|
);
|
||||||
|
let [isActive, setActive] = createSignal(false);
|
||||||
|
return (
|
||||||
|
<div class={props.class}>
|
||||||
|
<input
|
||||||
|
type={props.type}
|
||||||
|
class={
|
||||||
|
"form-control" + (!isActive() || props.valid ? "" : " is-invalid")
|
||||||
|
}
|
||||||
|
id={props.id}
|
||||||
|
value={props.value}
|
||||||
|
onInput={props.onInput}
|
||||||
|
onFocusOut={() => setActive(true)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
"invalid-feedback" +
|
||||||
|
(!isActive() || props.valid ? " d-none" : " d-block")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.errorText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatedTextInput;
|
||||||
36
www/src/tools/event-emitter.js
Normal file
36
www/src/tools/event-emitter.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
function EventEmitter() {
|
||||||
|
let instance;
|
||||||
|
|
||||||
|
let listeners = {};
|
||||||
|
|
||||||
|
function on(eventName, callback) {
|
||||||
|
if (!listeners[eventName]) {
|
||||||
|
listeners[eventName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listeners[eventName].indexOf(callback) !== -1) return;
|
||||||
|
|
||||||
|
listeners[eventName].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchEvent(eventName, payload) {
|
||||||
|
if (!listeners[eventName]) return;
|
||||||
|
listeners[eventName].forEach((listener) => listener(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
function off(eventName, callback) {
|
||||||
|
if (!listeners[eventName]) return;
|
||||||
|
let index = listeners[eventName].indexOf(callback);
|
||||||
|
listeners[eventName].splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = {
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
dispatchEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventEmitter;
|
||||||
74
www/src/tools/url-utils.js
Normal file
74
www/src/tools/url-utils.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
function UrlUtils() {
|
||||||
|
function getUrl() {
|
||||||
|
return window.location.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUrl(url) {
|
||||||
|
history.pushState(null, null, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQueryParameter(url, key, value) {
|
||||||
|
let [baseUrl, queryString] = url.split("?");
|
||||||
|
let queryParameters = queryString ? queryString.split("&") : [];
|
||||||
|
|
||||||
|
let hasKey = false;
|
||||||
|
for (let i = 0; i < queryParameters.length; i++) {
|
||||||
|
const [paramKey] = queryParameters[i].split("=");
|
||||||
|
if (paramKey === key) {
|
||||||
|
queryParameters[i] = `${key}=${value}`;
|
||||||
|
hasKey = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasKey) {
|
||||||
|
queryParameters.push(`${key}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUrl = `${baseUrl}?${queryParameters.join("&")}`;
|
||||||
|
return newUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeQueryParameter(url, key) {
|
||||||
|
let [baseUrl, queryString] = url.split("?");
|
||||||
|
let queryParameters = queryString ? queryString.split("&") : [];
|
||||||
|
|
||||||
|
for (let i = 0; i < queryParameters.length; i++) {
|
||||||
|
const [paramKey] = queryParameters[i].split("=");
|
||||||
|
if (paramKey === key) {
|
||||||
|
queryParameters.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUrl = `${baseUrl}?${queryParameters.join("&")}`;
|
||||||
|
return newUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueryParameter(key) {
|
||||||
|
let url = getUrl();
|
||||||
|
let [baseUrl, queryString] = url.split("?");
|
||||||
|
let queryParameters = queryString ? queryString.split("&") : [];
|
||||||
|
|
||||||
|
for (let i = 0; i < queryParameters.length; i++) {
|
||||||
|
const [paramKey, paramValue] = queryParameters[i].split("=");
|
||||||
|
if (paramKey === key) {
|
||||||
|
return decodeURIComponent(paramValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addQueryParameter,
|
||||||
|
removeQueryParameter,
|
||||||
|
getQueryParameter,
|
||||||
|
getUrl,
|
||||||
|
setUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlUtils = new UrlUtils();
|
||||||
|
|
||||||
|
export default UrlUtils;
|
||||||
@ -1,9 +1,213 @@
|
|||||||
|
import {
|
||||||
|
createSignal,
|
||||||
|
mergeProps,
|
||||||
|
createResource,
|
||||||
|
createEffect,
|
||||||
|
onMount,
|
||||||
|
onCleanup,
|
||||||
|
} from "solid-js";
|
||||||
|
import { computePosition, shift, autoUpdate, offset } from "@floating-ui/dom";
|
||||||
|
|
||||||
|
import UserService from "../services/user-service.js";
|
||||||
|
import ModalRegistry from "../modals/modal-registry.jsx";
|
||||||
|
import UrlUtils from "../tools/url-utils.js";
|
||||||
|
import SettingsView from "./settings-view.jsx";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEVICES_VIEW,
|
||||||
|
REMOTES_VIEW,
|
||||||
|
RECORDINGS_VIEW,
|
||||||
|
SETTINGS_VIEW,
|
||||||
|
} from "../data/constants.js";
|
||||||
|
|
||||||
|
let [activeView, setActiveView] = createSignal(DEVICES_VIEW);
|
||||||
|
|
||||||
const MainView = function (props) {
|
const MainView = function (props) {
|
||||||
|
props = mergeProps({ onLogout: () => {} }, props);
|
||||||
|
|
||||||
|
const [userInfo] = createResource(() => UserService.getUserInfo());
|
||||||
|
const [username, setUsername] = createSignal("");
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (userInfo.loading) return;
|
||||||
|
setUsername(userInfo().username);
|
||||||
|
});
|
||||||
|
|
||||||
|
let Modals = ModalRegistry.getModals();
|
||||||
|
|
||||||
|
let userButtonRef;
|
||||||
|
let userMenuRef;
|
||||||
|
let userMenuBackRef;
|
||||||
|
let cleanUserMenu;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setViewFromUrl();
|
||||||
|
window.addEventListener("popstate", setViewFromUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener("popstate", setViewFromUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setViewFromUrl() {
|
||||||
|
let view = UrlUtils.getQueryParameter("view");
|
||||||
|
if (view) {
|
||||||
|
setActiveView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
UserService.logout();
|
||||||
|
props.onLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserMenu() {
|
||||||
|
if (userMenuRef.style.display === "none") {
|
||||||
|
showUserMenu();
|
||||||
|
} else {
|
||||||
|
hideUserMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUserMenu() {
|
||||||
|
cleanUserMenu = autoUpdate(userButtonRef, userMenuRef, () =>
|
||||||
|
computePosition(userButtonRef, userMenuRef, {
|
||||||
|
placement: "bottom-end",
|
||||||
|
middleware: [offset(6), shift()],
|
||||||
|
}).then(({ x, y }) => {
|
||||||
|
Object.assign(userMenuRef.style, {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
userMenuRef.style.display = "block";
|
||||||
|
userMenuBackRef.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideUserMenu() {
|
||||||
|
userMenuRef.style.display = "none";
|
||||||
|
userMenuBackRef.style.display = "none";
|
||||||
|
cleanUserMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewChange(view) {
|
||||||
|
if (activeView() === view) return;
|
||||||
|
setActiveView(view);
|
||||||
|
let url = UrlUtils.getUrl();
|
||||||
|
url = UrlUtils.addQueryParameter(url, "view", view);
|
||||||
|
UrlUtils.setUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return <div class="d-flex flex-column" style="height:100vh"></div>;
|
return (
|
||||||
|
<div class="d-flex flex-column" style="height:100vh">
|
||||||
|
<Modals></Modals>
|
||||||
|
<HeaderBar></HeaderBar>
|
||||||
|
<ActiveView class={"bg-body-tertiary"}></ActiveView>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderBar() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="container-xl px-3 py-2 d-flex bg-dark-subtle">
|
||||||
|
<div class="d-flex align-items-center flex-fill">
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
"btn me-2" +
|
||||||
|
(activeView() === DEVICES_VIEW ? " btn-secondary" : " btn-dark")
|
||||||
|
}
|
||||||
|
onClick={() => handleViewChange(DEVICES_VIEW)}
|
||||||
|
>
|
||||||
|
<i class="bi-folder2 me-2"></i>
|
||||||
|
Devices
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
"btn me-2" +
|
||||||
|
(activeView() === REMOTES_VIEW ? " btn-secondary" : " btn-dark")
|
||||||
|
}
|
||||||
|
onClick={() => handleViewChange(REMOTES_VIEW)}
|
||||||
|
>
|
||||||
|
<i class="bi-gear-fill me-2"></i>
|
||||||
|
Remotes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
"btn me-2" +
|
||||||
|
(activeView() === RECORDINGS_VIEW
|
||||||
|
? " btn-secondary"
|
||||||
|
: " btn-dark")
|
||||||
|
}
|
||||||
|
onClick={() => handleViewChange(RECORDINGS_VIEW)}
|
||||||
|
>
|
||||||
|
<i class="bi bi-terminal me-2"></i>
|
||||||
|
Recordings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<button
|
||||||
|
ref={userButtonRef}
|
||||||
|
class="btn btn-dark rounded-circle p-0"
|
||||||
|
style="width: 2.4em; height: 2.4em"
|
||||||
|
onClick={toggleUserMenu}
|
||||||
|
>
|
||||||
|
<i class="bi bi-person-fill"></i>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
onClick={hideUserMenu}
|
||||||
|
ref={userMenuBackRef}
|
||||||
|
style="position:absolute;width:100vw;height:100vh;left:0;right:0;z-index:1060;display:none"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="list-group position-absolute"
|
||||||
|
style="display: none; z-index: 1070"
|
||||||
|
ref={userMenuRef}
|
||||||
|
>
|
||||||
|
<li class="list-group-item bg-dark-subtle">{username()}</li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
onClick={() => {
|
||||||
|
handleViewChange(SETTINGS_VIEW);
|
||||||
|
hideUserMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="bi bi-gear-fill me-2"></i>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="list-group-item list-group-item-action pe-auto"
|
||||||
|
onClick={() => handleLogout()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>
|
||||||
|
Log Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActiveView(props) {
|
||||||
|
return (
|
||||||
|
<div class="container-xl p-0 d-flex flex-column flex-fill overflow-hidden">
|
||||||
|
<Switch>
|
||||||
|
<Match when={activeView() === SETTINGS_VIEW}>
|
||||||
|
<SettingsView {...props} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return render();
|
return render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MainView.setActiveView = setActiveView;
|
||||||
|
|
||||||
export default MainView;
|
export default MainView;
|
||||||
|
|||||||
72
www/src/views/settings-view.jsx
Normal file
72
www/src/views/settings-view.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Show, createEffect, createResource, createSignal } from "solid-js";
|
||||||
|
import ChangePasswordSettings from "../modules/settings/change-password";
|
||||||
|
import UserList from "../modules/settings/users-list";
|
||||||
|
import UserService from "../services/user-service";
|
||||||
|
|
||||||
|
function SettingsView(props) {
|
||||||
|
const ACCOUNT_TAB = "account";
|
||||||
|
const USERS_TAB = "users";
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = createSignal(ACCOUNT_TAB);
|
||||||
|
const [userInfo] = createResource(() => UserService.getUserInfo());
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: ACCOUNT_TAB,
|
||||||
|
text: "Account",
|
||||||
|
render: renderAccountSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: USERS_TAB,
|
||||||
|
text: "Users",
|
||||||
|
adminOnly: true,
|
||||||
|
render: () => renderUserSettings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderAccountSettings() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ChangePasswordSettings />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserSettings() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UserList />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={"flex-fill p-3 d-flex flex-column " + props.class}>
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<h3 class="me-3 mb-0">Settings</h3>
|
||||||
|
<For each={tabs}>
|
||||||
|
{(tab) => (
|
||||||
|
<Show
|
||||||
|
when={!tab.adminOnly || (!userInfo.loading && userInfo().is_admin)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
"btn ms-2" +
|
||||||
|
(activeTab() === tab.id ? " btn-secondary" : " btn-dark")
|
||||||
|
}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.text}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<For each={tabs}>
|
||||||
|
{(tab) => <Show when={tab.id === activeTab()}>{tab.render()}</Show>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsView;
|
||||||
Loading…
Reference in New Issue
Block a user