Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
  • snuggle
  • userHandling
3 results

Target

Select target project
  • zbhai/ragchat-frontend
1 result
Select Git revision
  • main
  • snuggle
  • userHandling
3 results
Show changes

Commits on Source 5

Showing
with 421 additions and 86 deletions
@tailwind base;@tailwind components;@tailwind utilities;@layer base{.conceal{@apply opacity-0 h-0 w-0 p-0 m-0 overflow-hidden}label,details{@apply block w-full pb-1 relative cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))]}img,svg,video,canvas,audio,iframe,embed,object{display:inline;vertical-align:middle}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 204 98% 37%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 353 100% 44%;--destructive-foreground: 0 0% 98%;--border: 0 0% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--radius: 0.5rem}.dark{--background: 0 0% 3.9%;--foreground: 0 0% 98%;--card: 0 0% 3.9%;--card-foreground: 0 0% 98%;--popover: 0 0% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 0 0% 9%;--secondary: 0 0% 14.9%;--secondary-foreground: 0 0% 98%;--muted: 0 0% 14.9%;--muted-foreground: 0 0% 63.9%;--accent: 0 0% 14.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14.9%;--input: 0 0% 14.9%;--ring: 0 0% 83.1%}*{@apply border-border}body{@apply bg-background text-foreground}}/*# sourceMappingURL=tailwind.presets.min.css.map */ @tailwind base;@tailwind components;@tailwind utilities;@layer base{.conceal{@apply opacity-0 h-0 w-0 p-0 m-0 overflow-hidden}label,details{@apply block w-full pb-1 relative cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))]}fieldset{@apply pb-4 flex flex-wrap lg:gap-x-8 gap-4}img,svg,video,canvas,audio,iframe,embed,object{display:inline;vertical-align:middle}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 204 98% 37%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 353 100% 44%;--destructive-foreground: 0 0% 98%;--border: 0 0% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--radius: 0.5rem}.dark{--background: 0 0% 3.9%;--foreground: 0 0% 98%;--card: 0 0% 3.9%;--card-foreground: 0 0% 98%;--popover: 0 0% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 0 0% 9%;--secondary: 0 0% 14.9%;--secondary-foreground: 0 0% 98%;--muted: 0 0% 14.9%;--muted-foreground: 0 0% 63.9%;--accent: 0 0% 14.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14.9%;--input: 0 0% 14.9%;--ring: 0 0% 83.1%}*{@apply border-border}body{@apply bg-background text-foreground}}/*# sourceMappingURL=tailwind.presets.min.css.map */
\ No newline at end of file \ No newline at end of file
{"version":3,"sources":["../sass/tailwind.presets.scss"],"names":[],"mappings":"AAAA,cAAA,CACA,oBAAA,CACA,mBAAA,CACA,YACE,SACE,gDAAA,CAEF,cAEE,6MAAA,CAEF,+CAQE,cAAA,CACA,qBAAA,CAEF,MACE,uBAAA,CACA,uBAAA,CAEA,iBAAA,CACA,4BAAA,CAEA,oBAAA,CACA,+BAAA,CAGA,sBAAA,CACA,8BAAA,CAEA,uBAAA,CACA,+BAAA,CAEA,mBAAA,CACA,8BAAA,CAEA,oBAAA,CACA,4BAAA,CAEA,2BAAA,CACA,kCAAA,CAEA,oBAAA,CACA,mBAAA,CACA,iBAAA,CAEA,gBAAA,CAGF,MACE,uBAAA,CACA,sBAAA,CAEA,iBAAA,CACA,2BAAA,CAEA,oBAAA,CACA,8BAAA,CAEA,mBAAA,CACA,6BAAA,CAEA,uBAAA,CACA,gCAAA,CAEA,mBAAA,CACA,8BAAA,CAEA,oBAAA,CACA,6BAAA,CAEA,4BAAA,CACA,kCAAA,CAEA,oBAAA,CACA,mBAAA,CACA,kBAAA,CAGF,EACE,oBAAA,CAEF,KACE,oCAAA,CAAA","file":"tailwind.presets.min.css"} {"version":3,"sources":["../sass/tailwind.presets.scss"],"names":[],"mappings":"AAAA,cAAA,CACA,oBAAA,CACA,mBAAA,CACA,YACE,SACE,gDAAA,CAEF,cAEE,6MAAA,CAEF,SACE,2CAAA,CAEF,+CAQE,cAAA,CACA,qBAAA,CAEF,MACE,uBAAA,CACA,uBAAA,CAEA,iBAAA,CACA,4BAAA,CAEA,oBAAA,CACA,+BAAA,CAGA,sBAAA,CACA,8BAAA,CAEA,uBAAA,CACA,+BAAA,CAEA,mBAAA,CACA,8BAAA,CAEA,oBAAA,CACA,4BAAA,CAEA,2BAAA,CACA,kCAAA,CAEA,oBAAA,CACA,mBAAA,CACA,iBAAA,CAEA,gBAAA,CAGF,MACE,uBAAA,CACA,sBAAA,CAEA,iBAAA,CACA,2BAAA,CAEA,oBAAA,CACA,8BAAA,CAEA,mBAAA,CACA,6BAAA,CAEA,uBAAA,CACA,gCAAA,CAEA,mBAAA,CACA,8BAAA,CAEA,oBAAA,CACA,6BAAA,CAEA,4BAAA,CACA,kCAAA,CAEA,oBAAA,CACA,mBAAA,CACA,kBAAA,CAGF,EACE,oBAAA,CAEF,KACE,oCAAA,CAAA","file":"tailwind.presets.min.css"}
\ No newline at end of file \ No newline at end of file
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
details { details {
@apply block w-full pb-1 relative cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))]; @apply block w-full pb-1 relative cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))];
} }
fieldset {
@apply pb-4 flex flex-wrap lg:gap-x-8 gap-4;
}
img, img,
svg, svg,
video, video,
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Chat from './Chat';
import Heading from '../font/Heading';
import { RiAddCircleLine, RiArrowLeftCircleLine, RiArrowRightCircleLine } from 'react-icons/ri'; import { RiAddCircleLine, RiArrowLeftCircleLine, RiArrowRightCircleLine } from 'react-icons/ri';
import { useChat } from '../../contexts/Chat/ChatState';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useChat } from '../../contexts/Chat/ChatState';
import { mergeBackendValidation } from '../../utils/ErrorHandling'; import { mergeBackendValidation } from '../../utils/ErrorHandling';
import Heading from '../font/Heading';
import Chat from './Chat';
const Chats = () => { const Chats = () => {
// ################################# // #################################
......
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import Message from './Message';
import { useChat } from '../../contexts/Chat/ChatState';
import { Link } from 'react-router-dom';
import { RxBookmark } from 'react-icons/rx'; import { RxBookmark } from 'react-icons/rx';
import { Link } from 'react-router-dom';
import { useChat } from '../../contexts/Chat/ChatState';
import Message from './Message';
const Messages = () => { const Messages = () => {
......
import React, { useContext, useReducer, useState } from 'react'; import React, { useContext, useReducer, useState } from 'react';
import api from '../../utils/AxiosConfig';
import AuthContext from './AuthContext'; import AuthContext from './AuthContext';
import authReducer from './AuthReducer'; import authReducer from './AuthReducer';
import { USER_ACTIONS } from './AuthTypes'; import { USER_ACTIONS } from './AuthTypes';
import api from '../../utils/AxiosConfig';
// ### EXPORT useContext TO REDUCE NEEDED CODE IN CLIENT FILES // ### EXPORT useContext TO REDUCE NEEDED CODE IN CLIENT FILES
export function useAuth() { export function useAuth() {
...@@ -59,6 +59,16 @@ function AuthState({ children }) { ...@@ -59,6 +59,16 @@ function AuthState({ children }) {
return api.post('/auth/password-reset', { email }); return api.post('/auth/password-reset', { email });
} }
// ### UPDATE USER_ACTIONS
async function update(id, user) {
// remove password keys if not set to avoid emtying password field
if (user && !user.password) delete user.password;
if (user && !user.confirmPassword) delete user.confirmPassword;
// send data to backend
return api.patch(`/users/${id}`,
user
);
}
// ### RETURN // ### RETURN
return ( return (
...@@ -66,6 +76,7 @@ function AuthState({ children }) { ...@@ -66,6 +76,7 @@ function AuthState({ children }) {
value={{ value={{
login, login,
currentUser, currentUser,
update,
logout, logout,
requestVerificationToken, requestVerificationToken,
requestPasswordReset, requestPasswordReset,
......
import React from 'react'; import React from 'react';
import { useAuth } from '../../../contexts/Auth/AuthState';
import DesktopLink from './DesktopLink'; import DesktopLink from './DesktopLink';
function DesktopNav({ filteredSitemap }) { function DesktopNav({ filteredSitemap }) {
// ################################# // #################################
// HOOKS // HOOKS
// ################################# // #################################
// ### CONNECT AUTH CONTEXT
const { currentUser } = useAuth();
// ################################# // #################################
// FUNCTIONS // FUNCTIONS
...@@ -12,9 +15,15 @@ function DesktopNav({ filteredSitemap }) { ...@@ -12,9 +15,15 @@ function DesktopNav({ filteredSitemap }) {
// recursively render given menu // recursively render given menu
const renderMenu = (menu, parent = null) => { const renderMenu = (menu, parent = null) => {
if (!menu) return; if (!menu) return;
return menu.map((item, idx) => ( return menu.filter((item) => {
// dont show items that are above the current user role
if (item.gateKeeper && currentUser && currentUser.role < item.gateKeeper) return;
return item;
}).map((item, idx) => (
// render menu items
<li key={`link-${idx}`} className="relative" > <li key={`link-${idx}`} className="relative" >
{item.children?.length ? ( {
item.children?.length ? (
<> <>
<DesktopLink to={parent ? `${parent.path}/${item.path}` : item.path}>{item.title}</DesktopLink> <DesktopLink to={parent ? `${parent.path}/${item.path}` : item.path}>{item.title}</DesktopLink>
<ul className="absolute z-50 h-0 overflow-y-hidden bg-UhhBlue border-UhhBlue hover:h-auto peer-hover:h-auto"> <ul className="absolute z-50 h-0 overflow-y-hidden bg-UhhBlue border-UhhBlue hover:h-auto peer-hover:h-auto">
...@@ -23,9 +32,11 @@ function DesktopNav({ filteredSitemap }) { ...@@ -23,9 +32,11 @@ function DesktopNav({ filteredSitemap }) {
</> </>
) : ( ) : (
<DesktopLink to={parent ? `${parent.path}/${item.path}` : item.path}>{item.title}</DesktopLink> <DesktopLink to={parent ? `${parent.path}/${item.path}` : item.path}>{item.title}</DesktopLink>
)} )
}
</li> </li>
)); ));
}; };
// ################################# // #################################
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { sitemap } from "/src/routes/Sitemap"; import { useAuth } from '../../../contexts/Auth/AuthState';
import DesktopNav from './DesktopNav'; import DesktopNav from './DesktopNav';
import MobileNav from './MobileNav'; import MobileNav from './MobileNav';
import { sitemap } from "/src/routes/Sitemap";
function Navbar(props) { function Navbar(props) {
...@@ -11,6 +12,9 @@ function Navbar(props) { ...@@ -11,6 +12,9 @@ function Navbar(props) {
// ### FILTERED SITEMAP // ### FILTERED SITEMAP
const [filteredSitemap, setFilteredSitemap] = useState([]); const [filteredSitemap, setFilteredSitemap] = useState([]);
// ### CONNECT AUTH CONTEXT
const { currentUser } = useAuth();
useEffect(() => { useEffect(() => {
// fetch all links for navbars // fetch all links for navbars
const [overall] = sitemap.filter((item) => item.title === 'MenuBar'); const [overall] = sitemap.filter((item) => item.title === 'MenuBar');
...@@ -22,7 +26,12 @@ function Navbar(props) { ...@@ -22,7 +26,12 @@ function Navbar(props) {
function flatFilter(nestedProp, searchKey, searchValue, arr) { function flatFilter(nestedProp, searchKey, searchValue, arr) {
return arr.filter(o => { return arr.filter(o => {
// slightly customized for searchKey = object // slightly customized for searchKey = object
const keep = o[searchKey] && o[searchKey].hasOwnProperty(searchValue); let keep = o[searchKey] && o[searchKey].hasOwnProperty(searchValue);
// dont show items that are above the current user role
if (o.gateKeeper && currentUser && currentUser.role < o.gateKeeper) keep = false;
if (keep && o[nestedProp]) { if (keep && o[nestedProp]) {
o[nestedProp] = flatFilter(nestedProp, searchKey, searchValue, o[nestedProp]); o[nestedProp] = flatFilter(nestedProp, searchKey, searchValue, o[nestedProp]);
} }
......
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAuth } from '/src/contexts/Auth/AuthState'; import { RiDeleteBinLine, RiFileInfoLine, RiMoreLine, RiRefreshLine } from 'react-icons/ri';
import api from '/src/utils/AxiosConfig';
import ConfirmBox from '/src/components/boxes/ConfirmBox'; import ConfirmBox from '/src/components/boxes/ConfirmBox';
import InfoBox from '/src/components/boxes/InfoBox'; import InfoBox from '/src/components/boxes/InfoBox';
import CustomTable from '/src/components/table/customTable'; import CustomTable from '/src/components/table/customTable';
import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling'; import { Button } from '/src/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
...@@ -13,8 +12,9 @@ import { ...@@ -13,8 +12,9 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "/src/components/ui/dropdown-menu"; } from "/src/components/ui/dropdown-menu";
import { Button } from '/src/components/ui/button'; import { useAuth } from '/src/contexts/Auth/AuthState';
import { RiDeleteBinLine, RiFileInfoLine, RiMoreLine, RiRefreshLine } from 'react-icons/ri'; import api from '/src/utils/AxiosConfig';
import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling';
function Models({ data, setData }) { function Models({ data, setData }) {
...@@ -152,7 +152,7 @@ function Models({ data, setData }) { ...@@ -152,7 +152,7 @@ function Models({ data, setData }) {
// OUTPUT // OUTPUT
// ################################# // #################################
return ( return (
<div> <div className='self-start bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3'>
{/* table */} {/* table */}
<CustomTable columns={columns} data={data} title='installed models' /> <CustomTable columns={columns} data={data} title='installed models' />
......
...@@ -51,15 +51,17 @@ function NewModel({ data, setData }) { ...@@ -51,15 +51,17 @@ function NewModel({ data, setData }) {
return ( return (
<> <>
{(currentUser?.role >= 2) ? {(currentUser?.role >= 2) ?
<div className='self-start bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3'>
<FormProvider {...methods} > <FormProvider {...methods} >
<Heading level="4">install new model</Heading> <Heading level="4">install new model</Heading>
<form onSubmit={methods.handleSubmit(handleInstall)} className='md:w-1/3'> <form onSubmit={methods.handleSubmit(handleInstall)} className=''>
<Input name='model' type='text' title='Model Name' className='h-16' required={true} tooltip={<Link to='https://ollama.com/library' target='_blank' rel='noopener noreferrer'>Ollama Library</Link>} /> <Input name='model' type='text' title='Model Name' className='h-16' required={true} tooltip={<Link to='https://ollama.com/library' target='_blank' rel='noopener noreferrer'>Ollama Library</Link>} />
<Submit size='sm' value={methods.formState.isSubmitting ? 'installing...' : 'install model'} /> <Submit size='sm' value={methods.formState.isSubmitting ? 'installing...' : 'install model'} />
</form> </form>
</FormProvider> </FormProvider>
: null}
</div>
: null}
</> </>
); );
} }
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { mergeBackendValidation } from '../../../utils/ErrorHandling';
import api from '../../../utils/AxiosConfig';
import { RiWifiFill, RiWifiOffFill } from "react-icons/ri"; import { RiWifiFill, RiWifiOffFill } from "react-icons/ri";
import Heading from '../../../components/font/Heading'; import Heading from '../../../components/font/Heading';
import api from '../../../utils/AxiosConfig';
import { mergeBackendValidation } from '../../../utils/ErrorHandling';
function AIStatus() { function AIStatus() {
// ################################# // #################################
...@@ -43,11 +43,13 @@ function AIStatus() { ...@@ -43,11 +43,13 @@ function AIStatus() {
// OUTPUT // OUTPUT
// ################################# // #################################
return ( return (
<Heading level="4">status: <div className='self-start bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3'>
<Heading level="4">status <br />
{status ? {status ?
<RiWifiFill className='ml-4 text-UhhBlue' title='AI backend reachable' /> <RiWifiFill className='ml-4 text-UhhBlue' title='AI backend reachable' />
: <RiWifiOffFill className='ml-4 text-UhhRed' title='AI backend offline' />} : <RiWifiOffFill className='ml-4 text-UhhRed' title='AI backend offline' />}
</Heading> </Heading>
</div>
); );
} }
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import api from '../../utils/AxiosConfig'; import api from '../../utils/AxiosConfig';
import { mergeBackendValidation } from '../../utils/ErrorHandling'; import { mergeBackendValidation } from '../../utils/ErrorHandling';
import NewModel from './AI/NewModel';
import Models from './AI/Models'; import Models from './AI/Models';
import NewModel from './AI/NewModel';
import AIStatus from './AI/Status'; import AIStatus from './AI/Status';
function AIModels() { function AIModels() {
...@@ -62,17 +62,15 @@ function AIModels() { ...@@ -62,17 +62,15 @@ function AIModels() {
<div className="absolute w-[50vw] right-[50%] h-1 bg-UhhRed"></div> <div className="absolute w-[50vw] right-[50%] h-1 bg-UhhRed"></div>
</div> </div>
<div className='max-h-full flex flex-col'> <fieldset>
<div> {/* ai status */}
<AIStatus /> <AIStatus />
</div> {/* new model */}
<div>
<NewModel data={data} setData={setData} /> <NewModel data={data} setData={setData} />
</div> {/* model list */}
<div className='overflow-y-auto'>
<Models data={data} setData={setData} /> <Models data={data} setData={setData} />
</div>
</div> </fieldset>
......
...@@ -55,20 +55,14 @@ function Embeddings() { ...@@ -55,20 +55,14 @@ function Embeddings() {
<div className="absolute w-[50vw] right-[50%] h-1 bg-UhhRed"></div> <div className="absolute w-[50vw] right-[50%] h-1 bg-UhhRed"></div>
</div> </div>
<div className='max-h-full flex flex-col'> <fieldset>
<div>
{/* rag status */} {/* rag status */}
<Status status={status} /> <Status status={status} />
</div>
<div>
{/* update embeddings */} {/* update embeddings */}
<Update setStatus={setStatus} /> <Update setStatus={setStatus} />
</div>
<div>
{/* delete embeddings */} {/* delete embeddings */}
<Delete setStatus={setStatus} /> <Delete setStatus={setStatus} />
</div> </fieldset>
</div>
</section> </section>
); );
} }
......
...@@ -44,9 +44,9 @@ function Delete({ setStatus }) { ...@@ -44,9 +44,9 @@ function Delete({ setStatus }) {
// OUTPUT // OUTPUT
// ################################# // #################################
return ( return (
<div>
{(currentUser?.role >= 2) ?
<> <>
{(currentUser?.role >= 2) ?
<div className='self-start bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3'>
<FormProvider {...methods} > <FormProvider {...methods} >
<Heading level="4">Delete Embedding Collection</Heading> <Heading level="4">Delete Embedding Collection</Heading>
<form onSubmit={methods.handleSubmit(handleConfirm)} className='md:w-1/3'> <form onSubmit={methods.handleSubmit(handleConfirm)} className='md:w-1/3'>
...@@ -57,10 +57,10 @@ function Delete({ setStatus }) { ...@@ -57,10 +57,10 @@ function Delete({ setStatus }) {
<ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleDelete(confirmDialog.idToDelete); }} /> <ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleDelete(confirmDialog.idToDelete); }} />
</> </div>
: null} : null}
</div> </>
); );
} }
......
...@@ -15,7 +15,7 @@ function Status({ status }) { ...@@ -15,7 +15,7 @@ function Status({ status }) {
// OUTPUT // OUTPUT
// ################################# // #################################
return ( return (
<div> <div className='self-start bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3'>
<Heading level="4">Status</Heading> <Heading level="4">Status</Heading>
<JsonToHtmlDL jsonContent={status} /> <JsonToHtmlDL jsonContent={status} />
</div> </div>
......
...@@ -48,6 +48,7 @@ function Update({ setStatus }) { ...@@ -48,6 +48,7 @@ function Update({ setStatus }) {
return ( return (
<> <>
{(currentUser?.role >= 2) ? {(currentUser?.role >= 2) ?
<div className='self-start bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3'>
<FormProvider {...methods}> <FormProvider {...methods}>
<Heading level="4">Update Embeddings <Heading level="4">Update Embeddings
<Tooltip><p className='text-base'>based on local RAG Files</p></Tooltip> <Tooltip><p className='text-base'>based on local RAG Files</p></Tooltip>
...@@ -60,6 +61,7 @@ function Update({ setStatus }) { ...@@ -60,6 +61,7 @@ function Update({ setStatus }) {
<JsonToHtmlDL jsonContent={data} /> <JsonToHtmlDL jsonContent={data} />
</details> </details>
</FormProvider> </FormProvider>
</div>
: null} : null}
</> </>
); );
......
import { zodResolver } from '@hookform/resolvers/zod';
import React, { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { RiDeleteBinLine } from 'react-icons/ri';
import { z } from 'zod';
import Input from '../../../components/form/Input';
import Select from '../../../components/form/Select';
import Submit from '../../../components/form/Submit';
import { useAuth } from '../../../contexts/Auth/AuthState';
import { ROLES } from '../UserTypes';
import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling';
function User({ user, idx, setConfirmDialog }) {
// #################################
// VALIDATION SCHEMA
// #################################
const schema = z.object({
name: z.string().min(2),
username: z.string().min(2),
email: z.string().min(1).email(),
// turn string into boolean
verified: z.string().toLowerCase().transform((x) => x === 'true').pipe(z.boolean()),
role: z.coerce.number()
});
// #################################
// HOOKS
// #################################
// ### CONNECT AUTH CONTEXT
const { currentUser, update } = useAuth();
const [updatedUser, setUpdatedUser] = useState(user);
// ### PREPARE FORM
const methods = useForm({
resolver: zodResolver(schema),
mode: 'onChange',
// use anything from user, but turn verified into string
defaultValues: { ...user, verified: user.verified.toString() }
});
// const id = useId();
// #################################
// FUNCTIONS
// #################################
// ### HANDLE SUBMITTING FORM
async function handleSendForm(inputs) {
// save to db
try {
// send data
const result = await update(user._id, inputs);
setUpdatedUser(result.data.document);
// set flash msg
setFlashMsg(result.data?.message);
} catch (error) {
// catch the error
mergeBackendValidation(error.response.status, error.response.data, methods.setError);
}
}
// #################################
// OUTPUT
// #################################
return (
<div className='flex items-center bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3 m-3'>
<div>
{currentUser?.role >= 2 ?
<>
<h2 className='text-Uhh-Grey font-bold text-2xl flex justify-end'>
<RiDeleteBinLine className='cursor-pointer hover:text-UhhRed' title='delete user' onClick={() => setConfirmDialog({ open: true, idToDelete: user.id, displayName: user.username })} />
</h2>
<FormProvider {...methods} >
<form onSubmit={methods.handleSubmit(handleSendForm)}>
<Input name='name' type='text' title='name' required={true} />
<Input name='username' type='text' title='username' required={true} />
<Input name='email' type='email' title='eMail' required={true} />
<Select title="verified" name="verified" options={[{ _id: true, title: 'verified' }, { _id: false, title: 'not verified' }]} required={true} />
<Select title="Role" name="role" options={ROLES} required={true} />
<Submit value='save' />
</form>
</FormProvider>
</>
:
<>
<h2 className='text-Uhh-Grey font-bold text-2xl'>{user.fullname}</h2>
<p className='text-sm'>{user.email}</p>
</>
}
</div>
</div>
);
}
export default React.memo(User);
\ No newline at end of file
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import ConfirmBox from '../../../components/boxes/ConfirmBox';
import Heading from '../../../components/font/Heading';
import api from '../../../utils/AxiosConfig';
import { setFlashMsg } from '../../../utils/ErrorHandling';
import User from './User';
import { mergeBackendValidation } from '/src/utils/ErrorHandling';
function Users() {
// #################################
// HOOKS
// #################################
// ### USERS
const [users, setUsers] = useState();
// ### CONFIRM DIALOG
const [confirmDialog, setConfirmDialog] = useState({ open: false, item: {} });
// ### FETCH USERS
useEffect(() => {
// mount
const controller = new AbortController();
const getUsers = async () => {
try {
const result = await api.get('/users', {
signal: controller.signal
});
setUsers(result?.data);
} catch (error) {
mergeBackendValidation(error.response.status, error.response.data);
}
};
getUsers();
return () => {
// on unmount abort request
controller.abort();
};
}, []);
// #################################
// FUNCTIONS
// #################################
// ### DELETE USERS
const handleDelete = async (id) => {
try {
// delete in backend
const result = await api.delete(`/users/${id}`);
// delete in frontend
const list = users.filter(user => user.id !== id);
setUsers(list);
setFlashMsg(result.data?.message);
} catch (error) {
console.log("🚀 ~ handleDelete ~ error:", error);
mergeBackendValidation(error.response.status, error.response.data, methods.setError);
}
};
// #################################
// OUTPUT
// #################################
return (
<>
{/* render page title */}
<Helmet><title>[{import.meta.env.VITE_APP_NAME}] Users</title></Helmet>
<Heading level="1" className="col-span-2">Registered Users</Heading>
<div>
{users?.length
? (
<div className='flex flex-wrap justify-items-stretch'>
{users.map((user, idx) =>
<User key={user._id} idx={idx} user={user} setConfirmDialog={setConfirmDialog} />
)}
</div>
) : <p>No users to display</p>
}
</div>
{/* confirmDialog */}
<ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleDelete(confirmDialog.idToDelete); }} />
</>
);
}
export default React.memo(Users);
import { zodResolver } from '@hookform/resolvers/zod';
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import { FormProvider, useForm } from 'react-hook-form';
import { isStrongPassword } from 'validator';
import { z } from "zod";
import Input from '../..//components/form/Input';
import Heading from '../../components/font/Heading';
import Submit from '../../components/form/Submit';
import { useAuth } from '../../contexts/Auth/AuthState';
import { mergeBackendValidation, setFlashMsg } from '../../utils/ErrorHandling';
function Profile() {
// #################################
// VALIDATION SCHEMA
// #################################
// TODO limit file size via .env
// TODO check for file types
const schema = z.object({
name: z.string().min(1),
username: z.string().min(1),
email: z.string().email(),
password: z.string().refine((val) => val && isStrongPassword(val), {
message: 'This field must be min 6 characters long and contain uppercase, lowercase, number, specialchar.',
}).nullish().or(z.literal('')),
confirmPassword: z.string().nullish().or(z.literal('')),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
// #################################
// HOOKS
// #################################
// ### CONNECT AUTH CONTEXT
const { currentUser, dispatchCurrentUser, USER_ACTIONS, update } = useAuth();
// ### PREPARE FORM
const methods = useForm({
resolver: zodResolver(schema),
mode: 'onSubmit',
defaultValues: currentUser
});
// ### RESET FORM AFTER SUCCESSFUL SUBMIT
useEffect(() => {
if (!methods.formState.isSubmitSuccessful) return;
methods.reset(currentUser);
methods.setValue('confirmPassword', '');
methods.setValue('password', '');
}, [methods.formState]);
// #################################
// FUNCTIONS
// #################################
// ### HANDLE SUBMITTING FORM
async function handleSendForm(inputs) {
// TRY UPDATE
try {
// send data to update function
const result = await update(
currentUser._id,
inputs
);
// update currentUser
await dispatchCurrentUser({ type: USER_ACTIONS.SET, payload: result.data.document });
setFlashMsg(result.data?.message);
} catch (error) {
// catch the error
mergeBackendValidation(error.response.status, error.response.data, methods.setError);
}
}
// #################################
// OUTPUT
// #################################
return (
<>
{/* render page title */}
<Helmet><title>[{import.meta.env.VITE_APP_NAME}] Profile</title></Helmet>
<Heading level="1" className="col-span-2">Profile</Heading>
<FormProvider {...methods} >
<form onSubmit={methods.handleSubmit(handleSendForm)}>
<fieldset>
<Input name='name' type='text' title='Name' required={true} autoFocus={true} />
<Input name='username' type='text' title='Username' required={true} />
<Input name='email' type='email' title='eMail' required={true} />
</fieldset>
<fieldset>
<Input name='password' type='password' title='password' placeholder='Leave blank to keep the same' />
<Input name='confirmPassword' type='password' title='confirm password' placeholder='Leave blank to keep the same' />
</fieldset>
<Submit value='update' />
</form>
</FormProvider>
</>
);
}
export default React.memo(Profile);;
\ No newline at end of file
// the _id represents the order ranking the role
// the lower the _id, the lower the permissions
export const ROLES = [
{ _id: 0, title: 'User' },
{ _id: 1, title: 'Editor' },
{ _id: 2, title: 'Poweruser' },
{ _id: 3, title: 'Lead' },
{ _id: 4, title: 'Admin' }
];
\ No newline at end of file