[REFACTOR] remove all <dialog> modals and replace with custom ModalWrapper component (#641)

* add useModal hook and ModalWrapper component that will be used to replace all <dialog> modals for better browser support

* implement useModal hook and ModalWrapper component to replace all exisiting <dialog>
This commit is contained in:
Sean Hatfield 2024-01-23 14:19:08 -08:00 committed by GitHub
parent 2f3db0e63a
commit 62da5c9933
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 732 additions and 755 deletions

View File

@ -6,44 +6,42 @@ export default function ChangeWarningModal({
onConfirm,
}) {
return (
<dialog id="confirmation-modal" className="bg-transparent outline-none">
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<div className="flex items-center gap-2">
<Warning
className="text-yellow-300 text-lg w-6 h-6"
weight="fill"
/>
<h3 className="text-xl font-semibold text-yellow-300">Warning</h3>
</div>
</div>
<div className="w-[550px] p-6 text-white">
<p>
{warningText}
<br />
<br />
Are you sure you want to proceed?
</p>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={onClose}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-red-500 transition-all duration-300"
>
Cancel
</button>
<button
onClick={onConfirm}
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Confirm
</button>
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<div className="flex items-center gap-2">
<Warning
className="text-yellow-300 text-lg w-6 h-6"
weight="fill"
/>
<h3 className="text-xl font-semibold text-yellow-300">Warning</h3>
</div>
</div>
<div className="w-[550px] p-6 text-white">
<p>
{warningText}
<br />
<br />
Are you sure you want to proceed?
</p>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={onClose}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-red-500 transition-all duration-300"
>
Cancel
</button>
<button
onClick={onConfirm}
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Confirm
</button>
</div>
</div>
</dialog>
</div>
);
}

View File

@ -0,0 +1,9 @@
export default function ModalWrapper({ children, isOpen }) {
if (!isOpen) return null;
return (
<div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30">
{children}
</div>
);
}

View File

@ -193,7 +193,7 @@ function AccountModal({ user, hideModal }) {
return (
<div
id="account-modal"
className="bg-black/20 fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center"
className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center"
>
<div className="relative w-[500px] max-w-2xl max-h-full bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">

View File

@ -1,9 +1,10 @@
import { memo, useState, useEffect, useRef } from "react";
import { memo, useState } from "react";
import { X } from "@phosphor-icons/react";
import { v4 } from "uuid";
import { decode as HTMLDecode } from "he";
import { CaretRight, FileText } from "@phosphor-icons/react";
import truncate from "truncate";
import ModalWrapper from "@/components/ModalWrapper";
function combineLikeSources(sources) {
const combined = {};
@ -98,27 +99,10 @@ function SkeletonLine() {
function CitationDetailModal({ source, onClose }) {
const { references, title, text } = source;
const dialogRef = useRef(null);
useEffect(() => {
if (source && dialogRef.current) {
dialogRef.current.showModal();
}
}, [source]);
const handleModalClose = () => {
if (dialogRef.current) {
dialogRef.current.close();
}
onClose();
};
return (
<dialog
ref={dialogRef}
className="bg-transparent outline-none fixed top-0 left-0 w-full h-full flex items-center justify-center z-10"
>
<div className="relative w-full max-w-2xl bg-main-gradient rounded-lg shadow border border-white/10 overflow-hidden">
<ModalWrapper isOpen={source}>
<div className="w-full max-w-2xl bg-main-gradient rounded-lg shadow border border-white/10 overflow-hidden">
<div className="relative p-6 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
{truncate(title, 45)}
@ -129,7 +113,7 @@ function CitationDetailModal({ source, onClose }) {
</p>
)}
<button
onClick={handleModalClose}
onClick={onClose}
type="button"
className="absolute top-6 right-6 transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
@ -153,7 +137,7 @@ function CitationDetailModal({ source, onClose }) {
</div>
</div>
</div>
</dialog>
</ModalWrapper>
);
}

View File

@ -3,6 +3,7 @@ import Workspace from "@/models/workspace";
import LoadingChat from "./LoadingChat";
import ChatContainer from "./ChatContainer";
import paths from "@/utils/paths";
import ModalWrapper from "../ModalWrapper";
export default function WorkspaceChat({ loading, workspace }) {
const [history, setHistory] = useState([]);
@ -28,11 +29,7 @@ export default function WorkspaceChat({ loading, workspace }) {
return (
<>
{loading === false && !workspace && (
<dialog
open={true}
style={{ zIndex: 100 }}
className="fixed top-0 flex bg-black bg-opacity-50 w-full md:w-[100vw] h-full items-center justify-center"
>
<ModalWrapper isOpen={true}>
<div className="relative w-full md:max-w-2xl max-h-full bg-main-gradient rounded-lg shadow p-4">
<div className="flex flex-col gap-y-4 w-full p-6 text-center">
<p className="font-semibold text-red-500 text-xl">
@ -52,7 +49,7 @@ export default function WorkspaceChat({ loading, workspace }) {
</div>
</div>
</div>
</dialog>
</ModalWrapper>
)}
<LoadingChat />
</>

View File

@ -0,0 +1,10 @@
import { useState } from "react";
export function useModal() {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => setIsOpen(true);
const closeModal = () => setIsOpen(false);
return { isOpen, openModal, closeModal };
}

View File

@ -2,14 +2,7 @@ import React, { useEffect, useState } from "react";
import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
const DIALOG_ID = `new-invite-modal`;
function hideModal() {
document.getElementById(DIALOG_ID)?.close();
}
export const NewInviteModalId = DIALOG_ID;
export default function NewInviteModal() {
export default function NewInviteModal({ closeModal }) {
const [invite, setInvite] = useState(null);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
@ -39,74 +32,70 @@ export default function NewInviteModal() {
}, [copied]);
return (
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create new invite
</h3>
<button
onClick={hideModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
{invite && (
<input
type="url"
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
disabled={true}
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50"
/>
)}
<p className="text-white text-xs md:text-sm">
After creation you will be able to copy the invite and send it
to a new user where they can create an account as a default
user.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
{!invite ? (
<>
<button
onClick={hideModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create Invite
</button>
</>
) : (
<button
onClick={copyInviteLink}
type="button"
disabled={copied}
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
>
{copied ? "Copied Link" : "Copy Invite Link"}
</button>
)}
</div>
</form>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create new invite
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
{invite && (
<input
type="url"
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
disabled={true}
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50"
/>
)}
<p className="text-white text-xs md:text-sm">
After creation you will be able to copy the invite and send it
to a new user where they can create an account as a default
user.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
{!invite ? (
<>
<button
onClick={closeModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create Invite
</button>
</>
) : (
<button
onClick={copyInviteLink}
type="button"
disabled={copied}
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
>
{copied ? "Copied Link" : "Copy Invite Link"}
</button>
)}
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -7,9 +7,12 @@ import { EnvelopeSimple } from "@phosphor-icons/react";
import usePrefersDarkMode from "@/hooks/usePrefersDarkMode";
import Admin from "@/models/admin";
import InviteRow from "./InviteRow";
import NewInviteModal, { NewInviteModalId } from "./NewInviteModal";
import NewInviteModal from "./NewInviteModal";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function AdminInvites() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
@ -23,9 +26,7 @@ export default function AdminInvites() {
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Invitations</p>
<button
onClick={() =>
document?.getElementById(NewInviteModalId)?.showModal()
}
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
<EnvelopeSimple className="h-4 w-4" /> Create Invite Link
@ -38,7 +39,9 @@ export default function AdminInvites() {
</div>
<InvitationsContainer />
</div>
<NewInviteModal />
<ModalWrapper isOpen={isOpen}>
<NewInviteModal closeModal={closeModal} />
</ModalWrapper>
</div>
</div>
);

View File

@ -4,14 +4,7 @@ import Admin from "@/models/admin";
import { userFromStorage } from "@/utils/request";
import { RoleHintDisplay } from "..";
const DIALOG_ID = `new-user-modal`;
function hideModal() {
document.getElementById(DIALOG_ID)?.close();
}
export const NewUserModalId = DIALOG_ID;
export default function NewUserModal() {
export default function NewUserModal({ closeModal }) {
const [error, setError] = useState(null);
const [role, setRole] = useState("default");
const handleCreate = async (e) => {
@ -28,107 +21,103 @@ export default function NewUserModal() {
const user = userFromStorage();
return (
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Add user to instance
</h3>
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Add user to instance
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="username"
className="block mb-2 text-sm font-medium text-white"
>
Username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's username"
minLength={2}
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-white"
>
Password
</label>
<input
name="password"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's initial password"
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="role"
className="block mb-2 text-sm font-medium text-white"
>
Role
</label>
<select
name="role"
required={true}
defaultValue={"default"}
onChange={(e) => setRole(e.target.value)}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
>
<option value="default">Default</option>
<option value="manager">Manager </option>
{user?.role === "admin" && (
<option value="admin">Administrator</option>
)}
</select>
<RoleHintDisplay role={role} />
</div>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
<p className="text-white text-xs md:text-sm">
After creating a user they will need to login with their initial
login to get access.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={hideModal}
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
<X className="text-gray-300 text-lg" />
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Add user
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="username"
className="block mb-2 text-sm font-medium text-white"
>
Username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's username"
minLength={2}
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-white"
>
Password
</label>
<input
name="password"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's initial password"
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="role"
className="block mb-2 text-sm font-medium text-white"
>
Role
</label>
<select
name="role"
required={true}
defaultValue={"default"}
onChange={(e) => setRole(e.target.value)}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
>
<option value="default">Default</option>
<option value="manager">Manager </option>
{user?.role === "admin" && (
<option value="admin">Administrator</option>
)}
</select>
<RoleHintDisplay role={role} />
</div>
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
<p className="text-white text-xs md:text-sm">
After creating a user they will need to login with their
initial login to get access.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={hideModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Add user
</button>
</div>
</form>
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -3,16 +3,10 @@ import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import { RoleHintDisplay } from "../..";
export const EditUserModalId = (user) => `edit-user-${user.id}-modal`;
export default function EditUserModal({ currentUser, user }) {
export default function EditUserModal({ currentUser, user, closeModal }) {
const [role, setRole] = useState(user.role);
const [error, setError] = useState(null);
const hideModal = () => {
document.getElementById(EditUserModalId(user)).close();
};
const handleUpdate = async (e) => {
setError(null);
e.preventDefault();
@ -28,103 +22,99 @@ export default function EditUserModal({ currentUser, user }) {
};
return (
<dialog id={EditUserModalId(user)} className="bg-transparent outline-none">
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Edit {user.username}
</h3>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Edit {user.username}
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleUpdate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="username"
className="block mb-2 text-sm font-medium text-white"
>
Username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's username"
minLength={2}
defaultValue={user.username}
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-white"
>
New Password
</label>
<input
name="password"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder={`${user.username}'s new password`}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="role"
className="block mb-2 text-sm font-medium text-white"
>
Role
</label>
<select
name="role"
required={true}
defaultValue={user.role}
onChange={(e) => setRole(e.target.value)}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
>
<option value="default">Default</option>
<option value="manager">Manager</option>
{currentUser?.role === "admin" && (
<option value="admin">Administrator</option>
)}
</select>
<RoleHintDisplay role={role} />
</div>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={hideModal}
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
<X className="text-gray-300 text-lg" />
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Update user
</button>
</div>
<form onSubmit={handleUpdate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="username"
className="block mb-2 text-sm font-medium text-white"
>
Username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="User's username"
minLength={2}
defaultValue={user.username}
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-white"
>
New Password
</label>
<input
name="password"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder={`${user.username}'s new password`}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="role"
className="block mb-2 text-sm font-medium text-white"
>
Role
</label>
<select
name="role"
required={true}
defaultValue={user.role}
onChange={(e) => setRole(e.target.value)}
className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
>
<option value="default">Default</option>
<option value="manager">Manager</option>
{currentUser?.role === "admin" && (
<option value="admin">Administrator</option>
)}
</select>
<RoleHintDisplay role={role} />
</div>
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={hideModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Update user
</button>
</div>
</form>
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -1,9 +1,11 @@
import { useRef, useState } from "react";
import { titleCase } from "text-case";
import Admin from "@/models/admin";
import EditUserModal, { EditUserModalId } from "./EditUserModal";
import EditUserModal from "./EditUserModal";
import { DotsThreeOutline } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
const ModMap = {
admin: ["admin", "manager", "default"],
@ -15,6 +17,7 @@ export default function UserRow({ currUser, user }) {
const rowRef = useRef(null);
const canModify = ModMap[currUser?.role || "default"].includes(user.role);
const [suspended, setSuspended] = useState(user.suspended === 1);
const { isOpen, openModal, closeModal } = useModal();
const handleSuspend = async () => {
if (
!window.confirm(
@ -65,9 +68,7 @@ export default function UserRow({ currUser, user }) {
<td className="px-6 py-4 flex items-center gap-x-6">
{canModify && (
<button
onClick={() =>
document?.getElementById(EditUserModalId(user))?.showModal()
}
onClick={openModal}
className="font-medium text-white text-opacity-80 rounded-lg hover:text-white px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10"
>
<DotsThreeOutline weight="fill" className="h-5 w-5" />
@ -91,7 +92,13 @@ export default function UserRow({ currUser, user }) {
)}
</td>
</tr>
<EditUserModal currentUser={currUser} user={user} />
<ModalWrapper isOpen={isOpen}>
<EditUserModal
currentUser={currUser}
user={user}
closeModal={closeModal}
/>
</ModalWrapper>
</>
);
}

View File

@ -7,9 +7,12 @@ import { UserPlus } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import UserRow from "./UserRow";
import useUser from "@/hooks/useUser";
import NewUserModal, { NewUserModalId } from "./NewUserModal";
import NewUserModal from "./NewUserModal";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function AdminUsers() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
@ -23,9 +26,7 @@ export default function AdminUsers() {
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Users</p>
<button
onClick={() =>
document?.getElementById(NewUserModalId)?.showModal()
}
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
<UserPlus className="h-4 w-4" /> Add user
@ -39,7 +40,9 @@ export default function AdminUsers() {
</div>
<UsersContainer />
</div>
<NewUserModal />
<ModalWrapper isOpen={isOpen}>
<NewUserModal closeModal={closeModal} />
</ModalWrapper>
</div>
</div>
);

View File

@ -1,14 +1,8 @@
import React, { useState } from "react";
import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
const DIALOG_ID = `new-workspace-modal`;
function hideModal() {
document.getElementById(DIALOG_ID)?.close();
}
export const NewWorkspaceModalId = DIALOG_ID;
export default function NewWorkspaceModal() {
export default function NewWorkspaceModal({ closeModal }) {
const [error, setError] = useState(null);
const handleCreate = async (e) => {
setError(null);
@ -20,69 +14,65 @@ export default function NewWorkspaceModal() {
};
return (
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
<h3 className="text-xl font-semibold text-white">
Create new workspace
</h3>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
<h3 className="text-xl font-semibold text-white">
Create new workspace
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="name"
className="block mb-2 text-sm font-medium text-white"
>
Workspace name
</label>
<input
name="name"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="My workspace"
minLength={4}
required={true}
autoComplete="off"
/>
</div>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
<p className="text-white text-opacity-60 text-xs md:text-sm">
After creating this workspace only admins will be able to see
it. You can add users after it has been created.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600">
<button
onClick={hideModal}
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
<X className="text-gray-300 text-lg" />
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create workspace
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="name"
className="block mb-2 text-sm font-medium text-white"
>
Workspace name
</label>
<input
name="name"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="My workspace"
minLength={4}
required={true}
autoComplete="off"
/>
</div>
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
<p className="text-white text-opacity-60 text-xs md:text-sm">
After creating this workspace only admins will be able to see
it. You can add users after it has been created.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600">
<button
onClick={hideModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create workspace
</button>
</div>
</form>
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -6,13 +6,13 @@ import { titleCase } from "text-case";
export const EditWorkspaceUsersModalId = (workspace) =>
`edit-workspace-${workspace.id}-modal`;
export default function EditWorkspaceUsersModal({ workspace, users }) {
export default function EditWorkspaceUsersModal({
workspace,
users,
closeModal,
}) {
const [error, setError] = useState(null);
const hideModal = () => {
document.getElementById(EditWorkspaceUsersModalId(workspace)).close();
};
const handleUpdate = async (e) => {
setError(null);
e.preventDefault();
@ -35,122 +35,115 @@ export default function EditWorkspaceUsersModal({ workspace, users }) {
};
return (
<dialog
id={EditWorkspaceUsersModalId(workspace)}
className="bg-transparent outline-none"
>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Edit {workspace.name}
</h3>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Edit {workspace.name}
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleUpdate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{users
.filter((user) => user.role !== "admin")
.map((user) => {
return (
<div
key={`workspace-${workspace.id}-user-${user.id}`}
data-workspace={workspace.id}
className="flex items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
onClick={() => {
document
.getElementById(
`workspace-${workspace.id}-user-${user.id}`
)
?.click();
}}
>
<input
id={`workspace-${workspace.id}-user-${user.id}`}
defaultChecked={workspace.userIds.includes(user.id)}
type="checkbox"
value="yes"
name={`user-${user.id}`}
className="w-4 h-4 text-blue-600 bg-zinc-900 border border-gray-500/50 rounded focus:ring-blue-500 focus:border-blue-500 pointer-events-none"
/>
<label
htmlFor={`user-${user.id}`}
className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-white"
>
{titleCase(user.username)}
</label>
</div>
);
})}
<div className="flex items-center gap-x-4">
<button
type="button"
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
onClick={() => {
document
.getElementById(`workspace-${workspace.id}-select-all`)
?.click();
Array.from(
document.querySelectorAll(
`[data-workspace='${workspace.id}']`
)
).forEach((el) => {
if (!el.firstChild.checked) el.firstChild.click();
});
}}
>
Select All
</button>
<button
type="button"
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
onClick={() => {
document
.getElementById(`workspace-${workspace.id}-select-all`)
?.click();
Array.from(
document.querySelectorAll(
`[data-workspace='${workspace.id}']`
)
).forEach((el) => {
if (el.firstChild.checked) el.firstChild.click();
});
}}
>
Deselect All
</button>
</div>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={hideModal}
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
<X className="text-gray-300 text-lg" />
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Update workspace
</button>
</div>
<form onSubmit={handleUpdate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{users
.filter((user) => user.role !== "admin")
.map((user) => {
return (
<div
key={`workspace-${workspace.id}-user-${user.id}`}
data-workspace={workspace.id}
className="flex items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
onClick={() => {
document
.getElementById(
`workspace-${workspace.id}-user-${user.id}`
)
?.click();
}}
>
<input
id={`workspace-${workspace.id}-user-${user.id}`}
defaultChecked={workspace.userIds.includes(user.id)}
type="checkbox"
value="yes"
name={`user-${user.id}`}
className="w-4 h-4 text-blue-600 bg-zinc-900 border border-gray-500/50 rounded focus:ring-blue-500 focus:border-blue-500 pointer-events-none"
/>
<label
htmlFor={`user-${user.id}`}
className="pointer-events-none w-full py-4 ml-2 text-sm font-medium text-white"
>
{titleCase(user.username)}
</label>
</div>
);
})}
<div className="flex items-center gap-x-4">
<button
type="button"
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
onClick={() => {
document
.getElementById(`workspace-${workspace.id}-select-all`)
?.click();
Array.from(
document.querySelectorAll(
`[data-workspace='${workspace.id}']`
)
).forEach((el) => {
if (!el.firstChild.checked) el.firstChild.click();
});
}}
>
Select All
</button>
<button
type="button"
className="w-full p-4 flex text-white items-center pl-4 border border-gray-500/50 rounded group hover:bg-stone-900 transition-all duration-300 cursor-pointer"
onClick={() => {
document
.getElementById(`workspace-${workspace.id}-select-all`)
?.click();
Array.from(
document.querySelectorAll(
`[data-workspace='${workspace.id}']`
)
).forEach((el) => {
if (el.firstChild.checked) el.firstChild.click();
});
}}
>
Deselect All
</button>
</div>
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={hideModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Update workspace
</button>
</div>
</form>
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -1,13 +1,14 @@
import { useRef } from "react";
import Admin from "@/models/admin";
import paths from "@/utils/paths";
import EditWorkspaceUsersModal, {
EditWorkspaceUsersModalId,
} from "./EditWorkspaceUsersModal";
import EditWorkspaceUsersModal from "./EditWorkspaceUsersModal";
import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function WorkspaceRow({ workspace, users }) {
const rowRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal();
const handleDelete = async () => {
if (
!window.confirm(
@ -32,6 +33,7 @@ export default function WorkspaceRow({ workspace, users }) {
<a
href={paths.workspace.chat(workspace.slug)}
target="_blank"
rel="noreferrer"
className="text-white flex items-center hover:underline"
>
<LinkSimple className="mr-2 w-5 h-5" /> {workspace.slug}
@ -41,11 +43,7 @@ export default function WorkspaceRow({ workspace, users }) {
<td className="px-6 py-4">{workspace.createdAt}</td>
<td className="px-6 py-4 flex items-center gap-x-6">
<button
onClick={() =>
document
?.getElementById(EditWorkspaceUsersModalId(workspace))
?.showModal()
}
onClick={openModal}
className="font-medium rounded-lg hover:text-white hover:text-opacity-60 px-2 py-1 hover:bg-white hover:bg-opacity-10"
>
<DotsThreeOutline weight="fill" className="h-5 w-5" />
@ -58,7 +56,13 @@ export default function WorkspaceRow({ workspace, users }) {
</button>
</td>
</tr>
<EditWorkspaceUsersModal workspace={workspace} users={users} />
<ModalWrapper isOpen={isOpen}>
<EditWorkspaceUsersModal
workspace={workspace}
users={users}
closeModal={closeModal}
/>
</ModalWrapper>
</>
);
}

View File

@ -7,9 +7,12 @@ import { BookOpen } from "@phosphor-icons/react";
import usePrefersDarkMode from "@/hooks/usePrefersDarkMode";
import Admin from "@/models/admin";
import WorkspaceRow from "./WorkspaceRow";
import NewWorkspaceModal, { NewWorkspaceModalId } from "./NewWorkspaceModal";
import NewWorkspaceModal from "./NewWorkspaceModal";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function AdminWorkspaces() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
@ -25,9 +28,7 @@ export default function AdminWorkspaces() {
Instance workspaces
</p>
<button
onClick={() =>
document?.getElementById(NewWorkspaceModalId)?.showModal()
}
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
<BookOpen className="h-4 w-4" /> New Workspace
@ -40,7 +41,9 @@ export default function AdminWorkspaces() {
</div>
<WorkspacesContainer />
</div>
<NewWorkspaceModal />
<ModalWrapper isOpen={isOpen}>
<NewWorkspaceModal closeModal={closeModal} />
</ModalWrapper>
</div>
</div>
);

View File

@ -5,14 +5,7 @@ import paths from "@/utils/paths";
import { userFromStorage } from "@/utils/request";
import System from "@/models/system";
const DIALOG_ID = `new-api-key-modal`;
function hideModal() {
document.getElementById(DIALOG_ID)?.close();
}
export const NewApiKeyModalId = DIALOG_ID;
export default function NewApiKeyModal() {
export default function NewApiKeyModal({ closeModal }) {
const [apiKey, setApiKey] = useState(null);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
@ -43,80 +36,77 @@ export default function NewApiKeyModal() {
}, [copied]);
return (
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create new API key
</h3>
<button
onClick={hideModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
{apiKey && (
<input
type="text"
defaultValue={`${apiKey.secret}`}
disabled={true}
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50"
/>
)}
<p className="text-white text-xs md:text-sm">
Once created the API key can be used to programmatically
access and configure this AnythingLLM instance.
</p>
<a
href={paths.apiDocs()}
target="_blank"
className="text-blue-400 hover:underline"
>
Read the API documentation &rarr;
</a>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
{!apiKey ? (
<>
<button
onClick={hideModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create API key
</button>
</>
) : (
<button
onClick={copyApiKey}
type="button"
disabled={copied}
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
>
{copied ? "Copied API key" : "Copy API key"}
</button>
)}
</div>
</form>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create new API key
</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
{apiKey && (
<input
type="text"
defaultValue={`${apiKey.secret}`}
disabled={true}
className="rounded-lg px-4 py-2 text-white bg-zinc-900 border border-gray-500/50"
/>
)}
<p className="text-white text-xs md:text-sm">
Once created the API key can be used to programmatically access
and configure this AnythingLLM instance.
</p>
<a
href={paths.apiDocs()}
target="_blank"
rel="noreferrer"
className="text-blue-400 hover:underline"
>
Read the API documentation &rarr;
</a>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
{!apiKey ? (
<>
<button
onClick={closeModal}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Create API key
</button>
</>
) : (
<button
onClick={copyApiKey}
type="button"
disabled={copied}
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
>
{copied ? "Copied API key" : "Copy API key"}
</button>
)}
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -6,12 +6,15 @@ import "react-loading-skeleton/dist/skeleton.css";
import { PlusCircle } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import ApiKeyRow from "./ApiKeyRow";
import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal";
import NewApiKeyModal from "./NewApiKeyModal";
import paths from "@/utils/paths";
import { userFromStorage } from "@/utils/request";
import System from "@/models/system";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
export default function AdminApiKeys() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
@ -25,9 +28,7 @@ export default function AdminApiKeys() {
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">API Keys</p>
<button
onClick={() =>
document?.getElementById(NewApiKeyModalId)?.showModal()
}
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
<PlusCircle className="h-4 w-4" /> Generate New API Key
@ -40,6 +41,7 @@ export default function AdminApiKeys() {
<a
href={paths.apiDocs()}
target="_blank"
rel="noreferrer"
className="text-sm font-base text-blue-300 hover:underline"
>
Read the API documentation &rarr;
@ -47,7 +49,9 @@ export default function AdminApiKeys() {
</div>
<ApiKeysContainer />
</div>
<NewApiKeyModal />
<ModalWrapper isOpen={isOpen}>
<NewApiKeyModal closeModal={closeModal} />
</ModalWrapper>
</div>
</div>
);

View File

@ -2,9 +2,22 @@ import { useRef } from "react";
import truncate from "truncate";
import { X, Trash } from "@phosphor-icons/react";
import System from "@/models/system";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
export default function ChatRow({ chat }) {
const rowRef = useRef(null);
const {
isOpen: isPromptOpen,
openModal: openPromptModal,
closeModal: closePromptModal,
} = useModal();
const {
isOpen: isResponseOpen,
openModal: openResponseModal,
closeModal: closeResponseModal,
} = useModal();
const handleDelete = async () => {
if (
!window.confirm(
@ -30,17 +43,13 @@ export default function ChatRow({ chat }) {
</td>
<td className="px-6 py-4">{chat.workspace?.name}</td>
<td
onClick={() => {
document.getElementById(`chat-${chat.id}-prompt`)?.showModal();
}}
onClick={openPromptModal}
className="px-6 py-4 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
>
{truncate(chat.prompt, 40)}
</td>
<td
onClick={() => {
document.getElementById(`chat-${chat.id}-response`)?.showModal();
}}
onClick={openResponseModal}
className="px-6 py-4 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
>
{truncate(JSON.parse(chat.response)?.text, 40)}
@ -55,42 +64,38 @@ export default function ChatRow({ chat }) {
</button>
</td>
</tr>
<TextPreview text={chat.prompt} modalName={`chat-${chat.id}-prompt`} />
<TextPreview
text={JSON.parse(chat.response)?.text}
modalName={`chat-${chat.id}-response`}
/>
<ModalWrapper isOpen={isPromptOpen}>
<TextPreview text={chat.prompt} closeModal={closePromptModal} />
</ModalWrapper>
<ModalWrapper isOpen={isResponseOpen}>
<TextPreview
text={JSON.parse(chat.response)?.text}
closeModal={closeResponseModal}
/>
</ModalWrapper>
</>
);
}
function hideModal(modalName) {
document.getElementById(modalName)?.close();
}
const TextPreview = ({ text, modalName }) => {
const TextPreview = ({ text, closeModal }) => {
return (
<dialog id={modalName} className="bg-transparent outline-none w-full">
<div className="relative w-full md:max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
<button
onClick={() => hideModal(modalName)}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
data-modal-hide="staticModal"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="w-full p-6">
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 border border-gray-500 text-white text-sm">
{text}
</pre>
</div>
<div className="relative w-full md:max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="w-full p-6">
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 border border-gray-500 text-white text-sm">
{text}
</pre>
</div>
</div>
</dialog>
</div>
);
};

View File

@ -15,6 +15,8 @@ import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function GeneralEmbeddingPreference() {
const [saving, setSaving] = useState(false);
@ -25,6 +27,7 @@ export default function GeneralEmbeddingPreference() {
const [searchQuery, setSearchQuery] = useState("");
const [filteredEmbedders, setFilteredEmbedders] = useState([]);
const [selectedEmbedder, setSelectedEmbedder] = useState(null);
const { isOpen, openModal, closeModal } = useModal();
const handleSubmit = async (e) => {
e.preventDefault();
@ -33,7 +36,7 @@ export default function GeneralEmbeddingPreference() {
hasChanges &&
hasEmbeddings
) {
document.getElementById("confirmation-modal")?.showModal();
openModal();
} else {
await handleSaveSettings();
}
@ -56,7 +59,7 @@ export default function GeneralEmbeddingPreference() {
setHasChanges(false);
}
setSaving(false);
document.getElementById("confirmation-modal")?.close();
closeModal();
};
const updateChoice = (selection) => {
@ -116,11 +119,13 @@ export default function GeneralEmbeddingPreference() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<ChangeWarningModal
warningText=" Switching the embedder may affect previously embedded documents and future similarity search results."
onClose={() => document.getElementById("confirmation-modal")?.close()}
onConfirm={handleSaveSettings}
/>
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
{!isMobile && <Sidebar />}
{loading ? (
<div

View File

@ -21,6 +21,8 @@ import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"
import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem";
import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions";
import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function GeneralVectorDatabase() {
const [saving, setSaving] = useState(false);
@ -31,6 +33,7 @@ export default function GeneralVectorDatabase() {
const [searchQuery, setSearchQuery] = useState("");
const [filteredVDBs, setFilteredVDBs] = useState([]);
const [selectedVDB, setSelectedVDB] = useState(null);
const { isOpen, openModal, closeModal } = useModal();
useEffect(() => {
async function fetchKeys() {
@ -107,7 +110,7 @@ export default function GeneralVectorDatabase() {
const handleSubmit = async (e) => {
e.preventDefault();
if (selectedVDB !== settings?.VectorDB && hasChanges && hasEmbeddings) {
document.getElementById("confirmation-modal")?.showModal();
openModal();
} else {
await handleSaveSettings();
}
@ -130,7 +133,7 @@ export default function GeneralVectorDatabase() {
setHasChanges(false);
}
setSaving(false);
document.getElementById("confirmation-modal")?.close();
closeModal();
};
useEffect(() => {
@ -142,11 +145,13 @@ export default function GeneralVectorDatabase() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={() => document.getElementById("confirmation-modal")?.close()}
onConfirm={handleSaveSettings}
/>
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
{!isMobile && <Sidebar />}
{loading ? (
<div

View File

@ -31,71 +31,67 @@ export default function NewUserModal() {
};
return (
<dialog open={true} className="bg-transparent outline-none">
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create a new account
</h3>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="username"
className="block mb-2 text-sm font-medium text-white"
>
Username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="My username"
minLength={2}
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-white"
>
Password
</label>
<input
name="password"
type="password"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="Your password"
required={true}
minLength={8}
autoComplete="off"
/>
</div>
{error && (
<p className="text-red-400 text-sm">Error: {error}</p>
)}
<p className="text-slate-200 text-xs md:text-sm">
After creating your account you will be able to login with
these credentials and start using workspaces.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
type="submit"
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
>
Accept Invitation
</button>
</div>
</form>
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">
Create a new account
</h3>
</div>
<form onSubmit={handleCreate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="username"
className="block mb-2 text-sm font-medium text-white"
>
Username
</label>
<input
name="username"
type="text"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="My username"
minLength={2}
required={true}
autoComplete="off"
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-white"
>
Password
</label>
<input
name="password"
type="password"
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="Your password"
required={true}
minLength={8}
autoComplete="off"
/>
</div>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
<p className="text-slate-200 text-xs md:text-sm">
After creating your account you will be able to login with these
credentials and start using workspaces.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
type="submit"
className="w-full transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800 text-center justify-center"
>
Accept Invitation
</button>
</div>
</form>
</div>
</dialog>
</div>
);
}

View File

@ -3,6 +3,7 @@ import { useParams } from "react-router-dom";
import { FullScreenLoader } from "@/components/Preloader";
import Invite from "@/models/invite";
import NewUserModal from "./NewUserModal";
import ModalWrapper from "@/components/ModalWrapper";
export default function InvitePage() {
const { code } = useParams();
@ -47,7 +48,9 @@ export default function InvitePage() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center">
<NewUserModal />
<ModalWrapper isOpen={true}>
<NewUserModal />
</ModalWrapper>
</div>
);
}