This is an automated email from the ASF dual-hosted git repository.

mgubaidullin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit adb882c9e82b279ce8fd366b5ca29d7441bb15d0
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Mon Aug 24 17:56:05 2026 -0400

    Karavan-app UI Page Access
---
 .../main/webui/src/ui/page-access/AccessPage.css   |   0
 .../main/webui/src/ui/page-access/AccessPage.tsx   | 132 +++++++++++++++
 .../webui/src/ui/page-access/PasswordModal.tsx     | 143 ++++++++++++++++
 .../main/webui/src/ui/page-access/UserModal.tsx    | 156 ++++++++++++++++++
 .../src/ui/page-access/profile/ChangePassword.tsx  | 144 +++++++++++++++++
 .../src/ui/page-access/profile/UserProfile.tsx     | 110 +++++++++++++
 .../src/ui/page-access/profile/UserProfileTab.css  |  15 ++
 .../src/ui/page-access/profile/UserProfileTab.tsx  |  18 +++
 .../webui/src/ui/page-access/roles/RoleModal.tsx   | 112 +++++++++++++
 .../webui/src/ui/page-access/roles/RolesTable.tsx  |  60 +++++++
 .../src/ui/page-access/roles/RolesTableRow.tsx     |  90 +++++++++++
 .../src/ui/page-access/sessions/SessionTable.tsx   |  56 +++++++
 .../ui/page-access/sessions/SessionTableRow.tsx    |  88 ++++++++++
 .../webui/src/ui/page-access/users/UsersTable.tsx  |  62 +++++++
 .../src/ui/page-access/users/UsersTableRow.tsx     | 180 +++++++++++++++++++++
 15 files changed, 1366 insertions(+)

diff --git a/karavan-app/src/main/webui/src/ui/page-access/AccessPage.css 
b/karavan-app/src/main/webui/src/ui/page-access/AccessPage.css
new file mode 100644
index 00000000..e69de29b
diff --git a/karavan-app/src/main/webui/src/ui/page-access/AccessPage.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/AccessPage.tsx
new file mode 100644
index 00000000..e2eee314
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/AccessPage.tsx
@@ -0,0 +1,132 @@
+import React, {useEffect, useState} from 'react';
+import {Button, capitalize, Content, Tab, Tabs, TabTitleText, TextInputGroup, 
TextInputGroupMain, TextInputGroupUtilities,} from '@patternfly/react-core';
+import {useAccessStore} from "@stores/AccessStore";
+import {PlusIcon, SearchIcon, SyncAltIcon, TimesIcon} from 
"@patternfly/react-icons";
+import {RightPanel} from "@shared/ui/RightPanel";
+import {UsersTable} from "./users/UsersTable";
+import {ErrorBoundaryWrapper} from "@designer/ErrorBoundaryWrapper";
+import {RolesTable} from "./roles/RolesTable";
+import {UserModal} from "./UserModal";
+import {RoleModal} from "./roles/RoleModal";
+import {UserProfileTab} from "./profile/UserProfileTab";
+import {getCurrentUser} from "@api/auth/AuthApi";
+import {PLATFORM_ADMIN} from "@models/AccessModels";
+import {PasswordModal} from "./PasswordModal";
+import {SessionTable} from "./sessions/SessionTable";
+import {TabProps} from "@patternfly/react-core/src/components/Tabs/Tab";
+
+export const AccessPage = () => {
+
+    const adminMenus: (string | number)[] = ['profile', 'users', 'roles', 
'sessions'];
+    const userMenus: (string | number)[] = ['profile'];
+    const {
+        showUserModal, setShowUserModal, setFilter, filter, setCurrentUser, 
showRoleModal,
+        setShowRoleModal, showPasswordModal, showTokenModal, 
setShowTokenModal, refreshAccess
+    } = useAccessStore();
+    const [activeItem, setActiveItem] = useState<string | 
number>(userMenus.at(0)!);
+
+    const onNavSelect = (event: React.MouseEvent<HTMLElement, MouseEvent>, 
eventKey: TabProps['eventKey']) => {
+        setActiveItem(eventKey);
+    };
+
+    useEffect(() => {
+        refreshAccess();
+    }, []);
+
+    function searchInput() {
+        return (
+            <TextInputGroup className="search" style={{width: '300px'}}>
+                <TextInputGroupMain
+                    value={filter}
+                    placeholder='Search'
+                    type="text"
+                    autoComplete={"off"}
+                    autoFocus={true}
+                    icon={<SearchIcon/>}
+                    onChange={(_event, value) => {
+                        setFilter(value);
+                    }}
+                    aria-label="text input example"
+                />
+                <TextInputGroupUtilities>
+                    <Button variant="plain" onClick={_ => {
+                        setFilter('');
+                    }}>
+                        <TimesIcon aria-hidden={true}/>
+                    </Button>
+                </TextInputGroupUtilities>
+            </TextInputGroup>
+        )
+    }
+
+    function tools() {
+        const showAddButton = ["users", "roles", 
"tokens"].includes(activeItem?.toString())
+        return (<div className="project-files-toolbar" style={{justifyContent: 
"flex-end"}}>
+            <Button icon={<SyncAltIcon/>}
+                    variant={"link"}
+                    onClick={e => refreshAccess()}
+            />
+            {searchInput()}
+            {showAddButton &&
+                <Button className="dev-action-button"
+                        icon={<PlusIcon/>}
+                        onClick={e => {
+                            setCurrentUser(undefined)
+                            if (activeItem === "users") {
+                                setShowUserModal(true)
+                            } else if (activeItem === "roles") {
+                                setShowRoleModal(true)
+                            } else if (activeItem === "tokens") {
+                                setShowTokenModal(true)
+                            }
+                        }}
+                >Add</Button>
+            }
+        </div>);
+    }
+
+    function title() {
+        return (
+            <Content component="h2">Access Control</Content>
+        );
+    }
+
+    function getNavigation() {
+        return (
+            <Tabs onSelect={onNavSelect} isNav activeKey={activeItem}>
+                {(getCurrentUser()?.roles?.includes(PLATFORM_ADMIN) ? 
adminMenus : userMenus)
+                    .filter(m => []).map((item, i) =>
+                        <Tab
+                            key={item}
+                            eventKey={item}
+                            
title={<TabTitleText>{capitalize(item?.toString())}</TabTitleText>}
+                        />
+                    )}
+            </Tabs>
+        )
+    }
+
+    return (
+        <RightPanel
+            title={title()}
+            toolsStart={getNavigation()}
+            tools={undefined}
+            mainPanel={
+                <div className="right-panel-card">
+                    <ErrorBoundaryWrapper key='info' onError={error => 
console.error(error)}>
+                        <div style={{display: 'flex', flexDirection: 'column', 
height: '100%'}}>
+                            {activeItem !== 'profile' && tools()}
+                            {activeItem === 'profile' && <UserProfileTab/>}
+                            {activeItem === 'users' && <UsersTable/>}
+                            {activeItem === 'roles' && <RolesTable/>}
+                            {activeItem === 'sessions' && <SessionTable/>}
+                            {showUserModal && <UserModal/>}
+                            {showRoleModal && <RoleModal/>}
+                            {showPasswordModal && <PasswordModal/>}
+                        </div>
+                    </ErrorBoundaryWrapper>
+                </div>
+            }
+        />
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-access/PasswordModal.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/PasswordModal.tsx
new file mode 100644
index 00000000..e788ee8f
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/PasswordModal.tsx
@@ -0,0 +1,143 @@
+import React, {useEffect} from 'react';
+import {
+    Alert,
+    Button,
+    Content,
+    Form,
+    FormAlert,
+    FormGroup,
+    FormHelperText,
+    HelperText,
+    HelperTextItem,
+    Modal,
+    ModalBody,
+    ModalFooter,
+    ModalHeader,
+    ModalVariant,
+} from '@patternfly/react-core';
+import {SubmitHandler, useForm} from "react-hook-form";
+import {AxiosResponse} from "axios";
+import {useAccessStore} from "@stores/AccessStore";
+import {useFormUtil} from "@utils/useFormUtil";
+import {EventBus} from "@designer/utils/EventBus";
+import {shallow} from "zustand/shallow";
+import {AccessPassword} from "@models/AccessModels";
+import {AccessApi} from "@api/AccessApi";
+
+
+export function PasswordModal() {
+
+    const [showPasswordModal, setShowPasswordModal, currentUser]
+        = useAccessStore((s) => [s.showPasswordModal, s.setShowPasswordModal, 
s.currentUser], shallow);
+    const [isReset, setReset] = React.useState(false);
+    const [backendError, setBackendError] = React.useState<string>();
+    const formContext = useForm<AccessPassword>({mode: "all"});
+    const {getPasswordField} = useFormUtil(formContext);
+    const {
+        formState: {errors},
+        handleSubmit,
+        reset,
+        trigger
+    } = formContext;
+
+    useEffect(() => {
+        reset(new AccessPassword());
+        setBackendError(undefined);
+        setReset(true);
+    }, [reset]);
+
+    useEffect(() => {
+        isReset && trigger();
+    }, [trigger, isReset]);
+
+    const onSubmit: SubmitHandler<AccessPassword> = (data) => {
+        if (currentUser?.username)
+            AccessApi.setPassword(currentUser.username, data).then(value => 
after(value))
+    }
+
+    function after(result: [boolean, AxiosResponse | any]) {
+        if (result[0]) {
+            onSuccess();
+        } else {
+            const res = result[1]
+            const data = res?.response?.data;
+            const error = data && data !== '' ? data : res?.message;
+            setBackendError(error);
+        }
+    }
+
+    function onSuccess() {
+        const message = `Password successfully updated`;
+        EventBus.sendAlert("Success", message, "success");
+        closeModal()
+    }
+
+    function arePasswordsEqual() {
+        const pwd1 = formContext.getValues('password');
+        const pwd2 = formContext.getValues('password2');
+        return pwd1 === pwd2;
+    }
+
+    function canNotSubmit() {
+        return Object.getOwnPropertyNames(errors).length > 0 || 
!arePasswordsEqual();
+    }
+
+    function getPasswordError() {
+        if (!arePasswordsEqual()) {
+            return (<FormGroup>
+                <FormHelperText>
+                    <HelperText>
+                        <HelperTextItem variant={'error'}>
+                            Passwords should be equal!
+                        </HelperTextItem>
+                    </HelperText>
+                </FormHelperText>
+            </FormGroup>)
+        }
+    }
+
+    function closeModal() {
+        setShowPasswordModal(false)
+    }
+
+    function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
+        if (event.key === 'Enter') {
+            handleSubmit(onSubmit)()
+        }
+    }
+
+    return (
+        <Modal
+            variant={ModalVariant.small}
+            isOpen={showPasswordModal}
+            onClose={closeModal}
+            onKeyDown={onKeyDown}
+        >
+            <ModalHeader>
+                <Content component='h2'>{`Change password for user: 
${currentUser?.username}`}</Content>
+            </ModalHeader>
+            <ModalBody>
+                <Form isHorizontal={true} autoComplete="off">
+                    {getPasswordField('currentPassword', 'Your Password', {})}
+                    {getPasswordField('password', 'User Password', {})}
+                    {getPasswordField('password2', 'Retype Password', {})}
+                    {getPasswordError()}
+                    {backendError &&
+                        <FormAlert>
+                            <Alert variant="danger" title={backendError} 
aria-live="polite" isInline/>
+                        </FormAlert>
+                    }
+                </Form>
+            </ModalBody>
+            <ModalFooter>
+                <Button key="confirm" variant="primary"
+                        onClick={handleSubmit(onSubmit)}
+                        isDisabled={canNotSubmit()}
+                >
+                    Save
+                </Button>
+                <Button key="cancel" variant="secondary" 
onClick={closeModal}>Cancel</Button>
+            </ModalFooter>
+        </Modal>
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-access/UserModal.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/UserModal.tsx
new file mode 100644
index 00000000..9b5f252b
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/UserModal.tsx
@@ -0,0 +1,156 @@
+import React, {useEffect} from 'react';
+import {Alert, Button, Content, Form, FormAlert, FormGroup, Modal, ModalBody, 
ModalFooter, ModalHeader, ModalVariant, TextInput,} from 
'@patternfly/react-core';
+import {SubmitHandler, useForm} from "react-hook-form";
+import {AxiosResponse} from "axios";
+import {useAccessStore} from "@stores/AccessStore";
+import {useFormUtil} from "@utils/useFormUtil";
+import {EventBus} from "@designer/utils/EventBus";
+import {AccessApi} from "@api/AccessApi";
+import {getCurrentUser} from "@api/auth/AuthApi";
+import {AccessUser} from "@models/AccessModels";
+
+
+export function UserModal() {
+
+    const {showUserModal, setShowUserModal, currentUser, users, refreshAccess} 
= useAccessStore();
+    const [isReset, setReset] = React.useState(false);
+    const [backendError, setBackendError] = React.useState<string>();
+    const formContext = useForm<AccessUser>({mode: "all"});
+    const {getTextField} = useFormUtil(formContext);
+    const {
+        formState: {errors},
+        handleSubmit,
+        reset,
+        trigger
+    } = formContext;
+
+    useEffect(() => {
+        if (isNewUser()) {
+            reset(new AccessUser());
+        } else {
+            reset(currentUser);
+        }
+        setBackendError(undefined);
+        setReset(true);
+    }, [reset]);
+
+    useEffect(() => {
+        isReset && trigger();
+    }, [trigger, isReset]);
+
+    function closeModal() {
+        setShowUserModal(false)
+    }
+
+    const onSubmit: SubmitHandler<AccessUser> = (data) => {
+        if (isNewUser()) {
+            AccessApi.postUser(data).then((res) => after(res))
+        } else {
+            AccessApi.putUser(data).then((res) => after(res))
+        }
+    }
+
+    function after(result: [boolean, AxiosResponse | any]) {
+        const res = result[1];
+        if (result[0]) {
+            onSuccess(res.data);
+        } else {
+            const data = res?.response?.data;
+            const error = data && data !== '' ? data : res?.message;
+            setBackendError(error);
+        }
+    }
+
+    function onSuccess(user: AccessUser) {
+        const message = `User ${user.username} successfully ` + (isNewUser() ? 
"created" : "updated");
+        EventBus.sendAlert("Success", message, "success");
+        closeModal();
+        if (isNewUser()) {
+            refreshAccess();
+        } else {
+            if (getCurrentUser()?.username !== user.username) {
+                refreshAccess();
+            }
+        }
+    }
+
+    function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
+        if (event.key === 'Enter') {
+            handleSubmit(onSubmit)()
+        }
+    }
+
+    function isValidUsername(input: string): boolean {
+        const pattern = /^[a-z][a-z0-9-]*$/;
+        return pattern.test(input);
+    }
+
+    function isValidEmail(input: string): boolean {
+        const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+        return pattern.test(input);
+    }
+
+
+    function canNotSubmit() {
+        return Object.getOwnPropertyNames(errors).length > 0;
+    }
+
+    function isNewUser() {
+        return currentUser === undefined;
+    }
+
+    function itsMe() {
+        return currentUser?.username === getCurrentUser()?.username;
+    }
+
+    return (
+        <Modal
+            variant={ModalVariant.small}
+            isOpen={showUserModal}
+            onClose={closeModal}
+            onKeyDown={onKeyDown}
+        >
+            <ModalHeader>
+                <Content component='h2'>{isNewUser() ? "Add user" : "Update 
user"}</Content>
+            </ModalHeader>
+            <ModalBody>
+                <Form isHorizontal={true} autoComplete="off">
+                    {!isNewUser() &&
+                        <FormGroup label="Username" fieldId='username' 
isRequired>
+                            <TextInput className="text-field" type="text" 
id='username' value={currentUser?.username} isDisabled/>
+                        </FormGroup>
+                    }
+                    {isNewUser() && getTextField('username', 'Username', {
+                        regex: v => isValidUsername(v) || 'Only lowercase 
characters, numbers and dashes allowed',
+                        length: v => v.length > 2 || 'Username should be 
longer that 2 characters',
+                        name: v => !isNewUser() || !users.map(u => 
u.username).includes(v) || "User already exists!s",
+                    })}
+                    {getTextField('firstName', 'First Name', {
+                        length: v => v.length > 0 || 'First name should not be 
empty',
+                    })}
+                    {getTextField('lastName', 'Last Name', {
+                        length: v => v.length > 0 || 'Last name should not be 
empty',
+                    })}
+                    {getTextField('email', 'Email', {
+                        length: v => v.length > 0 || 'Last name should not be 
empty',
+                        email: v => isValidEmail(v) || 'Invalid email'
+                    }, 'email')}
+                    {backendError &&
+                        <FormAlert>
+                            <Alert variant="danger" title={backendError} 
aria-live="polite" isInline/>
+                        </FormAlert>
+                    }
+                </Form>
+            </ModalBody>
+            <ModalFooter>
+                <Button key="confirm" variant="primary"
+                        onClick={handleSubmit(onSubmit)}
+                        isDisabled={canNotSubmit()}
+                >
+                    Save
+                </Button>
+                <Button key="cancel" variant="secondary" 
onClick={closeModal}>Cancel</Button>
+            </ModalFooter>
+        </Modal>
+    )
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/profile/ChangePassword.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/profile/ChangePassword.tsx
new file mode 100644
index 00000000..342203ea
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/profile/ChangePassword.tsx
@@ -0,0 +1,144 @@
+import React, {useEffect, useState} from 'react';
+import {
+    Alert,
+    Button,
+    Card,
+    CardBody,
+    CardFooter,
+    CardHeader,
+    CardTitle,
+    Divider,
+    ExpandableSection,
+    Form,
+    FormAlert,
+    FormGroup,
+    FormHelperText,
+    HelperText,
+    HelperTextItem,
+} from '@patternfly/react-core';
+import {SubmitHandler, useForm} from "react-hook-form";
+import {AxiosResponse} from "axios";
+import {AccessPassword} from "@models/AccessModels";
+import {useFormUtil} from "@utils/useFormUtil";
+import {EventBus} from "@designer/utils/EventBus";
+import {AuthApi} from "@api/auth/AuthApi";
+
+
+export function ChangePassword() {
+
+    const [isReset, setReset] = React.useState(false);
+    const [backendError, setBackendError] = React.useState<string>();
+    const [isExpanded, setIsExpanded] = useState(false);
+    const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
+        setIsExpanded(isExpanded);
+    };
+    const formContext = useForm<AccessPassword>({mode: "all"});
+    const {getPasswordField} = useFormUtil(formContext);
+    const {
+        formState: {errors},
+        handleSubmit,
+        reset,
+        trigger
+    } = formContext;
+
+    useEffect(() => {
+        reset(new AccessPassword());
+        setBackendError(undefined);
+        setReset(true);
+    }, [reset]);
+
+    useEffect(() => {
+        isReset && trigger();
+    }, [trigger, isReset]);
+
+    const onSubmit: SubmitHandler<AccessPassword> = (data) => {
+        AuthApi.setPassword(data, after)
+    }
+
+    function after(result: boolean, res: AxiosResponse<AccessPassword> | any) {
+        if (result) {
+            onSuccess();
+            setIsExpanded(false);
+        } else {
+            const data = res?.response?.data;
+            const error = data && data !== '' ? data : res?.message;
+            setBackendError(error);
+        }
+    }
+
+    function onSuccess() {
+        const message = `Password successfully updated`;
+        EventBus.sendAlert("Success", message, "success");
+    }
+
+    function arePasswordsEqual() {
+        const pwd1 = formContext.getValues('password');
+        const pwd2 = formContext.getValues('password2');
+        return pwd1 === pwd2;
+    }
+
+    function canNotSubmit() {
+        return Object.getOwnPropertyNames(errors).length > 0 || 
!arePasswordsEqual();
+    }
+
+    function getPasswordError() {
+        if (!arePasswordsEqual()) {
+            return (<FormGroup>
+                <FormHelperText>
+                    <HelperText>
+                        <HelperTextItem variant={'error'}>
+                            Passwords should be equal!
+                        </HelperTextItem>
+                    </HelperText>
+                </FormHelperText>
+            </FormGroup>)
+        }
+    }
+
+    return (
+        <Card isCompact>
+            <CardHeader
+                actions={{
+                    hasNoOffset: false,
+                    actions: [
+                        <ExpandableSection
+                            key={"expandable-section"}
+                            toggleText={isExpanded ? 'Hide' : 'Change'}
+                            onToggle={onToggle}
+                            isExpanded={isExpanded}
+                        >
+                        </ExpandableSection>
+                    ]
+                }}
+            >
+                <CardTitle>Password</CardTitle>
+            </CardHeader>
+            {isExpanded && <Divider/>}
+            {isExpanded &&
+                <>
+                    <CardBody>
+                        <Form isHorizontal={true} autoComplete="off">
+                            {getPasswordField('currentPassword', 'Current 
Password', {})}
+                            {getPasswordField('password', 'Password', {})}
+                            {getPasswordField('password2', 'Retype Password', 
{})}
+                            {getPasswordError()}
+                            {backendError &&
+                                <FormAlert>
+                                    <Alert variant="danger" 
title={backendError} aria-live="polite" isInline/>
+                                </FormAlert>
+                            }
+                        </Form>
+                    </CardBody>
+                    <CardFooter style={{display: 'flex', justifyContent: 
'flex-end'}}>
+                        <Button key="confirm" variant="primary"
+                                onClick={handleSubmit(onSubmit)}
+                                isDisabled={canNotSubmit()}
+                        >
+                            Save
+                        </Button>
+                    </CardFooter>
+                </>
+            }
+        </Card>
+    )
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfile.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfile.tsx
new file mode 100644
index 00000000..680bcddb
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfile.tsx
@@ -0,0 +1,110 @@
+import React, {useEffect} from 'react';
+import {Alert, Button, Card, CardBody, CardFooter, CardHeader, Content, 
Divider, Form, FormAlert, FormGroup, TextInput,} from '@patternfly/react-core';
+import {SubmitHandler, useForm} from "react-hook-form";
+import {AxiosResponse} from "axios";
+import {AccessUser} from "@models/AccessModels";
+import {useFormUtil} from "@utils/useFormUtil";
+import {EventBus} from "@designer/utils/EventBus";
+import {AccessApi} from "@api/AccessApi";
+import {AuthApi, getCurrentUser} from "@api/auth/AuthApi";
+
+
+function UserProfile() {
+
+    const [isReset, setReset] = React.useState(false);
+    const [backendError, setBackendError] = React.useState<string>();
+    const formContext = useForm<AccessUser>({mode: "all"});
+    const {getTextField, getPasswordField} = useFormUtil(formContext);
+    const {
+        formState: {errors},
+        handleSubmit,
+        reset,
+        trigger
+    } = formContext;
+
+    useEffect(() => {
+        AuthApi.getMe(user => {
+        })
+    }, []);
+
+    useEffect(() => {
+        const user = getCurrentUser();
+        if (user) reset(user);
+        setBackendError(undefined);
+        setReset(true);
+    }, [reset]);
+
+    useEffect(() => {
+        isReset && trigger();
+    }, [trigger, isReset]);
+
+    const onSubmit: SubmitHandler<AccessUser> = (data) => {
+        AccessApi.putUser(data).then((res) => after(res))
+    }
+
+    function after(result: [boolean, AxiosResponse | any]) {
+        const res = result[1];
+        if (result[0]) {
+            onSuccess(res.data);
+        } else {
+            const data = res?.response?.data;
+            const error = data && data !== '' ? data : res?.message;
+            setBackendError(error);
+        }
+    }
+
+    function onSuccess(user: AccessUser) {
+        const message = `User ${user.username} successfully updated`;
+        EventBus.sendAlert("Success", message, "success");
+    }
+
+    function isValidEmail(input: string): boolean {
+        const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+        return pattern.test(input);
+    }
+
+    function canNotSubmit() {
+        return Object.getOwnPropertyNames(errors).length > 0;
+    }
+
+    return (
+        <Card isCompact>
+            <CardHeader>
+                <Content component='h2'>User</Content>
+            </CardHeader>
+            <Divider/>
+            <CardBody>
+                <Form isHorizontal={true} autoComplete="off">
+                    <FormGroup label="Username" fieldId='username' isRequired>
+                        <TextInput className="text-field" type="text" 
id='username' value={getCurrentUser()?.username} isDisabled/>
+                    </FormGroup>
+                    {getTextField('firstName', 'First Name', {
+                        length: v => v.length > 0 || 'First name should not be 
empty',
+                    })}
+                    {getTextField('lastName', 'Last Name', {
+                        length: v => v.length > 0 || 'Last name should not be 
empty',
+                    })}
+                    {getTextField('email', 'Email', {
+                        length: v => v.length > 0 || 'Last name should not be 
empty',
+                        email: v => isValidEmail(v) || 'Invalid email'
+                    }, 'email')}
+                    {backendError &&
+                        <FormAlert>
+                            <Alert variant="danger" title={backendError} 
aria-live="polite" isInline/>
+                        </FormAlert>
+                    }
+                </Form>
+            </CardBody>
+            <CardFooter style={{display: 'flex', justifyContent: 'flex-end'}}>
+                <Button key="confirm" variant="primary"
+                        onClick={handleSubmit(onSubmit)}
+                        isDisabled={canNotSubmit()}
+                >
+                    Save
+                </Button>
+            </CardFooter>
+        </Card>
+    )
+}
+
+export default UserProfile
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfileTab.css 
b/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfileTab.css
new file mode 100644
index 00000000..33ca7a79
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfileTab.css
@@ -0,0 +1,15 @@
+.user-profile-tab-wrapper {
+    display: flex;
+    flex-direction: column;
+    overflow: auto;
+    .user-profile-tab {
+        padding: 16px;
+        overflow: auto;
+        .user-profile-tab-panels {
+            display: flex;
+            flex-direction: column;
+            justify-content: start;
+            gap: 16px;
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfileTab.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfileTab.tsx
new file mode 100644
index 00000000..b978b535
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/profile/UserProfileTab.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import UserProfile from "./UserProfile";
+import {ChangePassword} from "./ChangePassword";
+import "./UserProfileTab.css"
+
+export function UserProfileTab() {
+
+    return (
+        <div className={"user-profile-tab-wrapper"}>
+            <div className={"user-profile-tab"}>
+                <div className={"user-profile-tab-panels"}>
+                    <UserProfile/>
+                    <ChangePassword/>
+                </div>
+            </div>
+        </div>
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-access/roles/RoleModal.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/roles/RoleModal.tsx
new file mode 100644
index 00000000..d6c1fef3
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/roles/RoleModal.tsx
@@ -0,0 +1,112 @@
+import React, {useEffect} from 'react';
+import {Alert, Button, Content, Form, FormAlert, Modal, ModalBody, 
ModalFooter, ModalHeader, ModalVariant,} from '@patternfly/react-core';
+import {SubmitHandler, useForm} from "react-hook-form";
+import {AxiosResponse} from "axios";
+import {AccessRole} from "@models/AccessModels";
+import {useAccessStore} from "@stores/AccessStore";
+import {useFormUtil} from "@utils/useFormUtil";
+import {EventBus} from "@designer/utils/EventBus";
+import {AccessApi} from "@api/AccessApi";
+
+export function RoleModal() {
+
+    const {showRoleModal, setShowRoleModal, roles, refreshAccess} = 
useAccessStore();
+    const [isReset, setReset] = React.useState(false);
+    const [backendError, setBackendError] = React.useState<string>();
+    const formContext = useForm<AccessRole>({mode: "all"});
+    const {getTextField} = useFormUtil(formContext);
+    const {
+        formState: {errors},
+        handleSubmit,
+        reset,
+        trigger
+    } = formContext;
+
+    useEffect(() => {
+        reset(new AccessRole());
+        setBackendError(undefined);
+        setReset(true);
+    }, [reset]);
+
+    useEffect(() => {
+        isReset && trigger();
+    }, [trigger, isReset]);
+
+    function closeModal() {
+        setShowRoleModal(false)
+    }
+
+    const onSubmit: SubmitHandler<AccessRole> = (data) => {
+        AccessApi.postRole(data).then(value => after(value))
+    }
+
+    function after(result: [boolean, AxiosResponse | any]) {
+        const res = result[1];
+        if (result[0]) {
+            onSuccess(res.data);
+        } else {
+            const data = res?.response?.data;
+            const error = data && data !== '' ? data : res?.message;
+            setBackendError(error);
+        }
+    }
+
+    function onSuccess(role: AccessRole) {
+        const message = `Role ${role.name} successfully created`;
+        EventBus.sendAlert("Success", message, "success");
+        closeModal();
+        refreshAccess();
+    }
+
+    function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
+        if (event.key === 'Enter') {
+            handleSubmit(onSubmit)()
+        }
+    }
+
+    function isValidName(input: string): boolean {
+        return !roles.map(r => r.name).includes(input);
+    }
+
+    function canNotSubmit() {
+        return Object.getOwnPropertyNames(errors).length > 0;
+    }
+
+    return (
+        <Modal
+            variant={ModalVariant.small}
+            isOpen={showRoleModal}
+            onClose={closeModal}
+            onKeyDown={onKeyDown}
+        >
+            <ModalHeader>
+                <Content component='h2'>Add role</Content>
+            </ModalHeader>
+            <ModalBody>
+                <Form isHorizontal={true} autoComplete="off">
+                    {getTextField('name', 'Name', {
+                        length: v => v.length > 0 || 'Name should not be 
empty',
+                        name: v => isValidName(v) || "Role already exists!s",
+                    })}
+                    {getTextField('description', 'Description', {
+                        length: v => v.length > 0 || 'Description should not 
be empty',
+                    })}
+                    {backendError &&
+                        <FormAlert>
+                            <Alert variant="danger" title={backendError} 
aria-live="polite" isInline/>
+                        </FormAlert>
+                    }
+                </Form>
+            </ModalBody>
+            <ModalFooter>
+                <Button key="confirm" variant="primary"
+                        onClick={handleSubmit(onSubmit)}
+                        isDisabled={canNotSubmit()}
+                >
+                    Save
+                </Button>
+                <Button key="cancel" variant="secondary" 
onClick={closeModal}>Cancel</Button>
+            </ModalFooter>
+        </Modal>
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-access/roles/RolesTable.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/roles/RolesTable.tsx
new file mode 100644
index 00000000..10a00645
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/roles/RolesTable.tsx
@@ -0,0 +1,60 @@
+import React, {useState} from 'react';
+import {Bullseye, EmptyState, EmptyStateVariant, Spinner} from 
'@patternfly/react-core';
+import '../AccessPage.css';
+import {InnerScrollContainer, OuterScrollContainer, Table, TableVariant, 
Tbody, Td, Th, Thead, Tr} from '@patternfly/react-table';
+import {SearchIcon} from "@patternfly/react-icons";
+import {shallow} from "zustand/shallow";
+import {useAccessStore} from "@stores/AccessStore";
+import {AccessRole} from "@models/AccessModels";
+import {RolesTableRow} from "./RolesTableRow";
+
+export function RolesTable() {
+
+    const [roles, filter] = useAccessStore((s) => [s.roles, s.filter], 
shallow);
+    const [loading] = useState<boolean>(true);
+
+    function getEmptyState() {
+        return (
+            <Tbody>
+                <Tr>
+                    <Td colSpan={8}>
+                        <Bullseye>
+                            {loading && <Spinner className="progress-stepper" 
diameter="80px" aria-label="Loading..."/>}
+                            {!loading &&
+                                <EmptyState variant={EmptyStateVariant.sm} 
titleText="No results found" icon={SearchIcon} headingLevel="h2"/>
+                            }
+                        </Bullseye>
+                    </Td>
+                </Tr>
+            </Tbody>
+        )
+    }
+
+    const conts = roles.filter(role =>
+        role.name?.toLowerCase().includes(filter)
+        || role.name?.toLowerCase().includes(filter)
+        || role.name?.toLowerCase().includes(filter)
+    ).sort((a, b) => a.name.localeCompare(b.name));
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table aria-label="Projects" variant={TableVariant.compact} 
isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th key='type' modifier={'fitContent'}>Type</Th>
+                            <Th key='name'>Name</Th>
+                            <Th key='decription'>Description</Th>
+                            <Th key='users'>Users</Th>
+                            <Th key='action' screenReaderText='pass'></Th>
+                        </Tr>
+                    </Thead>
+                    {conts?.map((role: AccessRole, index: number) => (
+                        <RolesTableRow key={role.name} index={index} 
role={role}/>
+                    ))}
+                    {conts?.length === 0 && getEmptyState()}
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/roles/RolesTableRow.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/roles/RolesTableRow.tsx
new file mode 100644
index 00000000..bd19f9ea
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/roles/RolesTableRow.tsx
@@ -0,0 +1,90 @@
+import React, {useState} from 'react';
+import {Button, capitalize, Content, Label} from '@patternfly/react-core';
+import {Tbody, Td, Tr} from "@patternfly/react-table";
+import {AccessRole, PLATFORM_ADMIN, PLATFORM_DEVELOPER, PLATFORM_USER} from 
"@models/AccessModels";
+import {useAccessStore} from "@stores/AccessStore";
+import {ShieldAltIcon, TimesIcon, UsersIcon} from "@patternfly/react-icons";
+import {ModalConfirmation} from "@shared/ui/ModalConfirmation";
+import {AccessApi} from "@api/AccessApi";
+
+interface Props {
+    index: number
+    role: AccessRole
+}
+
+export function RolesTableRow(props: Props) {
+
+    const {users, refreshAccess} = useAccessStore();
+    const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
+    const [command, setCommand] = useState<'create' | 'delete' >();
+
+    const {role} = props;
+
+    function executeAction() {
+        if (command === 'delete') {
+            AccessApi.deleteRole(role.name).then(_ => refreshAccess());
+        }
+        setShowConfirmation(false);
+    }
+
+    function getConfirmationText() {
+        if (command === 'delete') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='red'>{capitalize('' + command)}</Label>
+                    {" role "}
+                    {<Label color='blue'>{role.name}</Label>}
+                    {" ?"}
+                </div>
+            )
+        }
+    }
+
+    const usersWithRole = users.filter(user => user.roles?.includes(role.name))
+    const isBuildInRole = [PLATFORM_DEVELOPER, PLATFORM_USER, 
PLATFORM_ADMIN].includes(role?.name);
+    const canBeDeleted = !isBuildInRole && usersWithRole.length === 0;
+    return (
+        <Tbody>
+            <Tr key={role.name} style={{verticalAlign: 'middle'}}>
+                <Td>{isBuildInRole ? <ShieldAltIcon/> : <UsersIcon/>}</Td>
+                <Td>{role.name}</Td>
+                <Td>{role.description}</Td>
+                <Td modifier='fitContent'>
+                    <div style={{display: 'flex', flexDirection: 'column', 
gap: '4px'}}>
+                        {usersWithRole.map(user => {
+                            return (
+                                <Content 
key={user.username}>{user.username}</Content>
+                            )
+                        })}
+                    </div>
+                </Td>
+
+                <Td isActionCell>
+                    <Button className="dev-action-button"
+                            isDisabled={!canBeDeleted}
+                            variant={"plain"}
+                            icon={<TimesIcon/>}
+                            style={{padding: '6px', marginLeft: '6px'}}
+                            onClick={() => {
+                                setCommand('delete')
+                                setShowConfirmation(true);
+                            }}/>
+                </Td>
+            </Tr>
+            {showConfirmation &&
+                <ModalConfirmation
+                    isOpen={showConfirmation}
+                    message={getConfirmationText()}
+                    btnConfirm='Confirm'
+                    btnConfirmVariant='danger'
+                    onConfirm={() => {
+                        setCommand(undefined);
+                        setShowConfirmation(false);
+                        executeAction();
+                    }}
+                    onCancel={() => setShowConfirmation(false)}
+                />
+            }
+        </Tbody>
+    )
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/sessions/SessionTable.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/sessions/SessionTable.tsx
new file mode 100644
index 00000000..2ac439d0
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/sessions/SessionTable.tsx
@@ -0,0 +1,56 @@
+import React, {useState} from 'react';
+import {Bullseye, EmptyState, EmptyStateVariant, Spinner} from 
'@patternfly/react-core';
+import '../AccessPage.css';
+import {InnerScrollContainer, OuterScrollContainer, Table, TableVariant, 
Tbody, Td, Th, Thead, Tr} from '@patternfly/react-table';
+import {SearchIcon} from "@patternfly/react-icons";
+import {shallow} from "zustand/shallow";
+import {useAccessStore} from "@stores/AccessStore";
+import {SessionInfo} from "@models/AccessModels";
+import {SessionTableRow} from "./SessionTableRow";
+
+export function SessionTable() {
+
+    const [sessions, filter] = useAccessStore((s) => [s.sessions, s.filter], 
shallow);
+    const [loading] = useState<boolean>(true);
+
+    function getEmptyState() {
+        return (
+            <Tbody>
+                <Tr>
+                    <Td colSpan={8}>
+                        <Bullseye>
+                            {loading && <Spinner className="progress-stepper" 
diameter="80px" aria-label="Loading..."/>}
+                            {!loading &&
+                                <EmptyState variant={EmptyStateVariant.sm} 
titleText="No results found" icon={SearchIcon} headingLevel="h2"/>
+                            }
+                        </Bullseye>
+                    </Td>
+                </Tr>
+            </Tbody>
+        )
+    }
+
+    const conts = sessions.filter(session => 
session.username?.toLowerCase().includes(filter)
+    ).sort((a, b) => a.username.localeCompare(b.username));
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table aria-label="Projects" variant={TableVariant.compact} 
isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th key='name'>Name</Th>
+                            <Th key='created'>Created At</Th>
+                            <Th key='expired'>Expired At</Th>
+                            <Th key='action' screenReaderText='pass'></Th>
+                        </Tr>
+                    </Thead>
+                    {conts?.map((session: SessionInfo, index: number) => (
+                        <SessionTableRow key={session.username + "-" + index} 
index={index} session={session}/>
+                    ))}
+                    {conts?.length === 0 && getEmptyState()}
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/sessions/SessionTableRow.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/sessions/SessionTableRow.tsx
new file mode 100644
index 00000000..b3a53eb8
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/sessions/SessionTableRow.tsx
@@ -0,0 +1,88 @@
+import React, {useState} from 'react';
+import {Button, capitalize, Label} from '@patternfly/react-core';
+import {Tbody, Td, Tr} from "@patternfly/react-table";
+import {SessionInfo} from "@models/AccessModels";
+import {TimesIcon} from "@patternfly/react-icons";
+import {ModalConfirmation} from "@shared/ui/ModalConfirmation";
+import {AccessApi} from "@api/AccessApi";
+import {useAccessStore} from "@stores/AccessStore";
+import timeAgo from "@shared/timeAgo";
+
+interface Props {
+    index: number
+    session: SessionInfo
+}
+
+export function SessionTableRow(props: Props) {
+
+    const {refreshAccess} = useAccessStore();
+    const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
+    const [command, setCommand] = useState<'create' | 'delete' >();
+    const {session, index} = props;
+    const isExpired = session.expiredAt < new Date().getTime();
+
+
+    function executeAction() {
+        if (command === 'delete') {
+            AccessApi.deleteSession(session.username).then(_ => 
refreshAccess());
+        }
+        setShowConfirmation(false);
+    }
+
+    function getConfirmationText() {
+        if (command === 'delete') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='red'>{capitalize('' + command)}</Label>
+                    {" session for "}
+                    {<Label color='blue'>{session.username}</Label>}
+                    {" ?"}
+                </div>
+            )
+        }
+    }
+
+    return (
+        <Tbody>
+            <Tr key={index} style={{verticalAlign: 'middle'}}>
+                <Td>{session.username}</Td>
+                <Td>
+                    <div style={{display: 'flex', flexDirection: 'row', gap: 
32, alignItems: 'center'}}>
+                        {session.createdAt && new 
Date(session.createdAt).toISOString()}
+                        <Label color={isExpired ? 'red' : 
'blue'}>{timeAgo.format(new Date(session.createdAt))}</Label>
+                    </div>
+                </Td>
+                <Td>
+                    <div style={{display: 'flex', flexDirection: 'row', gap: 
32, alignItems: 'center'}}>
+                        {session.expiredAt && new 
Date(session.expiredAt).toISOString()}
+                        <Label color={isExpired ? 'red' : 
'blue'}>{timeAgo.format(new Date(session.expiredAt))}</Label>
+                    </div>
+                </Td>
+                <Td isActionCell>
+                    <Button className="dev-action-button"
+                            variant={"plain"}
+                            icon={<TimesIcon/>}
+                            style={{padding: '6px', marginLeft: '6px'}}
+                            onClick={() => {
+                                setCommand('delete')
+                                setShowConfirmation(true);
+                            }}/>
+                </Td>
+            </Tr>
+            {showConfirmation &&
+                <ModalConfirmation
+                    isOpen={showConfirmation}
+                    message={getConfirmationText()}
+                    btnConfirm='Confirm'
+                    btnConfirmVariant='danger'
+                    onConfirm={() => {
+                        setCommand(undefined);
+                        setShowConfirmation(false);
+                        executeAction();
+                    }}
+                    onCancel={() => setShowConfirmation(false)}
+                />
+            }
+        </Tbody>
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/ui/page-access/users/UsersTable.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/users/UsersTable.tsx
new file mode 100644
index 00000000..852770ed
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/users/UsersTable.tsx
@@ -0,0 +1,62 @@
+import React, {useState} from 'react';
+import {Bullseye, EmptyState, EmptyStateVariant, Spinner} from 
'@patternfly/react-core';
+import '../AccessPage.css';
+import {InnerScrollContainer, OuterScrollContainer, Table, TableVariant, 
Tbody, Td, Th, Thead, Tr} from '@patternfly/react-table';
+import {SearchIcon} from "@patternfly/react-icons";
+import {shallow} from "zustand/shallow";
+import {UsersTableRow} from "./UsersTableRow";
+import {useAccessStore} from "@stores/AccessStore";
+import {AccessUser} from "@models/AccessModels";
+
+export function UsersTable() {
+
+    const [users, filter] = useAccessStore((s) => [s.users, s.filter], 
shallow);
+    const [loading] = useState<boolean>(true);
+
+    function getEmptyState() {
+        return (
+            <Tbody>
+                <Tr>
+                    <Td colSpan={8}>
+                        <Bullseye>
+                            {loading && <Spinner className="progress-stepper" 
diameter="80px" aria-label="Loading..."/>}
+                            {!loading &&
+                                <EmptyState variant={EmptyStateVariant.sm} 
titleText="No results found" icon={SearchIcon} headingLevel="h2"/>
+                            }
+                        </Bullseye>
+                    </Td>
+                </Tr>
+            </Tbody>
+        )
+    }
+
+    const conts = users.filter(user =>
+        user.username?.toLowerCase().includes(filter)
+        || user.firstName?.toLowerCase().includes(filter)
+        || user.lastName?.toLowerCase().includes(filter)
+    ).sort((a, b) => a.username.localeCompare(b.username));
+    return (
+        <OuterScrollContainer>
+            <InnerScrollContainer>
+                <Table aria-label="Projects" variant={TableVariant.compact} 
isStickyHeader>
+                    <Thead>
+                        <Tr>
+                            <Th key='username'>Username</Th>
+                            <Th key='firstName'>First Name</Th>
+                            <Th key='lastName'>Last Name</Th>
+                            <Th key='email'>Email</Th>
+                            <Th key='roles'>Roles</Th>
+                            <Th key='status'>Status</Th>
+                            <Th key='action' screenReaderText='pass'></Th>
+                        </Tr>
+                    </Thead>
+                    {conts?.map((user: AccessUser, index: number) => (
+                        <UsersTableRow key={user.username} index={index} 
user={user}/>
+                    ))}
+                    {conts?.length === 0 && getEmptyState()}
+                </Table>
+            </InnerScrollContainer>
+        </OuterScrollContainer>
+    )
+
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-access/users/UsersTableRow.tsx 
b/karavan-app/src/main/webui/src/ui/page-access/users/UsersTableRow.tsx
new file mode 100644
index 00000000..349c8c21
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-access/users/UsersTableRow.tsx
@@ -0,0 +1,180 @@
+import React, {useState} from 'react';
+import {Button, capitalize, Label, Switch} from '@patternfly/react-core';
+import {Tbody, Td, Tr} from "@patternfly/react-table";
+import {AccessUser} from "@models/AccessModels";
+import {useAccessStore} from "@stores/AccessStore";
+import {AccessApi} from "@api/AccessApi";
+import {PauseIcon, PlayIcon, TimesIcon, UserSecretIcon} from 
"@patternfly/react-icons";
+import {ModalConfirmation} from "@shared/ui/ModalConfirmation";
+
+interface Props {
+    index: number
+    user: AccessUser
+}
+
+export function UsersTableRow(props: Props) {
+
+    const {setShowUserModal, setCurrentUser, roles, setShowPasswordModal, 
refreshAccess} = useAccessStore();
+    const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
+    const [command, setCommand] = useState<'activate' | 'inactivate' | 
'delete' | 'add' | 'remove'>();
+    const [role, setRole] = useState<string>();
+
+    const user = props.user;
+
+    function executeAction() {
+        if (command === 'delete') {
+            AccessApi.deleteUser(user.username).then(value => refreshAccess());
+        } else if (command && ['activate', 'inactivate'].includes(command)) {
+            const status = command === 'inactivate' ? 'INACTIVE' : 'ACTIVE';
+            AccessApi.setUserStatus(user, status).then(value => 
refreshAccess());
+        } else if (command && ['add', 'remove'].includes(command)) {
+            AccessApi.setUserRole(user, role, command).then(value => 
refreshAccess());
+        }
+        setShowConfirmation(false);
+    }
+
+    function getConfirmationText() {
+        if (command === 'delete') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='red'>{capitalize('' + command)}</Label>
+                    {" user "}
+                    {<Label color='blue'>{user.username}</Label>}
+                    {" ?"}
+                </div>
+            )
+        } else if (command === 'inactivate') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='red'>{capitalize('' + command)}</Label>
+                    {" user "}
+                    {<Label color='blue'>{user.username}</Label>}
+                    {" ?"}
+                </div>
+            )
+        } else if (command === 'activate') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='green'>{capitalize('' + command)}</Label>
+                    {" user "}
+                    {<Label color='blue'>{user.username}</Label>}
+                    {" ?"}
+                </div>
+            )
+        } else if (command === 'remove') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='red'>{capitalize('' + command)}</Label>
+                    {" user "}
+                    {<Label color='blue'>{user.username}</Label>}
+                    {" from role "}
+                    {<Label color='blue'>{role}</Label>}
+                    {" ?"}
+                </div>
+            )
+        } else if (command === 'add') {
+            return (
+                <div style={{ display: 'flex', flexDirection: 'row', gap: 
'4px', alignItems: 'center' }}>
+                    <Label color='green'>{capitalize('' + command)}</Label>
+                    {" user "}
+                    {<Label color='blue'>{user.username}</Label>}
+                    {" to role "}
+                    {<Label color='blue'>{role}</Label>}
+                    {" ?"}
+                </div>
+            )
+        }
+    }
+
+    const deactivatable = !['admin', 'platform'].includes(user?.username) && 
user.status !== 'DELETED';
+    const notDeletable = ['admin', 'platform'].includes(user?.username) || 
user.status === 'DELETED';
+    const notEditable = ['admin', 'platform'].includes(user?.username) || 
user.status !== 'ACTIVE'
+    return (
+        <Tbody>
+            <Tr key={user.username} style={{verticalAlign: 'middle'}}>
+                <Td>
+                    <Button variant='link' style={{padding: '6px'}} 
onClick={() => {
+                        setCurrentUser(user)
+                        setShowUserModal(true)
+                    }}>
+                        {user.username}
+                    </Button>
+                </Td>
+                <Td>{user.firstName}</Td>
+                <Td>{user.lastName}</Td>
+                <Td>{user.email}</Td>
+                <Td modifier='fitContent'>
+                    <div style={{display: 'flex', flexDirection: 'column', 
gap: '4px'}}>
+                        {roles.map(role => {
+                            const isChecked = user.roles.includes(role.name)
+                            return (
+                                <Switch
+                                    key={`${user.username}-${role.name}`}
+                                    id={`${user.username}-${role.name}`}
+                                    label={role.name}
+                                    className='switch-role'
+                                    isChecked={isChecked}
+                                    ouiaId={'admin'}
+                                    isDisabled={notEditable}
+                                    onClick={(_) => {
+                                        setRole(role.name);
+                                        setCommand(isChecked ? 'remove' : 
'add')
+                                        setShowConfirmation(true);
+                                    }}
+                                />
+                            )
+                        })}
+                    </div>
+                </Td>
+                <Td modifier='fitContent'>
+                    <Label color={user.status === 'ACTIVE' ? 'green' : 
(user.status === 'DELETED' ? "red" : 'grey')}>{user.status}</Label>
+                </Td>
+                <Td isActionCell>
+                    <div style={{display: 'flex', flexDirection: 'row', gap: 
'4px', justifyContent: 'flex-end'}}>
+                        {deactivatable && <Button className="dev-action-button"
+                                 variant={"link"}
+                                 icon={user.status === 'INACTIVE' ? 
<PlayIcon/> : <PauseIcon/>}
+                                 style={{padding: '6px', marginLeft: '6px'}}
+                                 onClick={() => {
+                                     setCommand(user.status === 'INACTIVE' ? 
'activate' : 'inactivate')
+                                     setShowConfirmation(true);
+                                 }}/>}
+                        <Button className="dev-action-button"
+                                isDisabled={notDeletable}
+                                variant={"plain"}
+                                icon={<TimesIcon/>}
+                                style={{padding: '6px', marginLeft: '6px'}}
+                                onClick={() => {
+                                    setCommand('delete')
+                                    setShowConfirmation(true);
+                                }}/>
+                        <Button className="dev-action-button"
+                                isDisabled={notDeletable}
+                                variant={"plain"}
+                                icon={<UserSecretIcon color={notDeletable ? 
'var(--pf-t--global--icon--color--disabled)' : 
'var(--pf-t--global--icon--color--status--danger--default)'}/>}
+                                style={{padding: '6px', marginLeft: '6px'}}
+                                onClick={() => {
+                                    setCurrentUser(user)
+                                    setShowPasswordModal(true);
+                                }}/>
+                    </div>
+                </Td>
+            </Tr>
+            {showConfirmation &&
+                <ModalConfirmation
+                    isOpen={showConfirmation}
+                    message={getConfirmationText()}
+                    btnConfirm='Confirm'
+                    btnConfirmVariant='danger'
+                    onConfirm={() => {
+                        setCommand(undefined);
+                        setRole(undefined);
+                        setShowConfirmation(false);
+                        executeAction();
+                    }}
+                    onCancel={() => setShowConfirmation(false)}
+                />
+            }
+        </Tbody>
+    )
+}
\ No newline at end of file

Reply via email to