[STYLE] Implement new settings sidebar UI ()

* implement new settings sidebar v2

* store state of settings menu in localstorage to improve ux

* add tailwind color

* add missed admin translation

* fix admin pages showing on single user

* perms fix for manager role

* refactor permissions for options on sidebar

* minor refactor of menuoption

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-07-10 12:20:06 -07:00 committed by GitHub
parent e7fe35bda9
commit 69d67401ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 368 additions and 230 deletions
frontend
src
components/SettingsSidebar
locales
tailwind.config.js

View file

@ -0,0 +1,168 @@
import React, { useEffect, useState } from "react";
import { CaretRight } from "@phosphor-icons/react";
import { Link } from "react-router-dom";
export default function MenuOption({
btnText,
icon,
href,
childOptions = [],
flex = false,
user = null,
roles = [],
hidden = false,
isChild = false,
}) {
const storageKey = generateStorageKey({ key: btnText });
const location = window.location.pathname;
const hasChildren = childOptions.length > 0;
const hasVisibleChildren = hasVisibleOptions(user, childOptions);
const { isExpanded, setIsExpanded } = useIsExpanded({
storageKey,
hasVisibleChildren,
childOptions,
location,
});
if (hidden) return null;
// If this option is a parent level option
if (!isChild) {
// and has no children then use its flex props and roles prop directly
if (!hasChildren) {
if (!flex && !roles.includes(user?.role)) return null;
if (flex && !!user && !roles.includes(user?.role)) return null;
}
// if has children and no visible children - remove it.
if (hasChildren && !hasVisibleChildren) return null;
} else {
// is a child so we use it's permissions
if (!flex && !roles.includes(user?.role)) return null;
if (flex && !!user && !roles.includes(user?.role)) return null;
}
const isActive = hasChildren
? (!isExpanded && childOptions.some((child) => child.href === location)) ||
location === href
: location === href;
const handleClick = (e) => {
if (hasChildren) {
e.preventDefault();
const newExpandedState = !isExpanded;
setIsExpanded(newExpandedState);
localStorage.setItem(storageKey, JSON.stringify(newExpandedState));
}
};
return (
<div>
<div
className={`
flex items-center justify-between w-full
transition-all duration-300
rounded-[6px]
${
isActive
? "bg-white/5 font-medium border-outline"
: "hover:bg-white/5"
}
`}
>
<Link
to={href}
className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${
isChild ? "text-white/70 hover:text-white" : "text-white"
}`}
onClick={hasChildren ? handleClick : undefined}
>
{icon}
<p
className={`${
isChild ? "text-xs" : "text-sm"
} leading-loose whitespace-nowrap overflow-hidden ml-2 ${
isActive ? "text-white" : ""
} ${!icon && "pl-5"}`}
>
{btnText}
</p>
</Link>
{hasChildren && (
<button onClick={handleClick} className="p-2 text-white">
<CaretRight
size={16}
weight="bold"
className={`transition-transform ${
isExpanded ? "rotate-90" : ""
}`}
/>
</button>
)}
</div>
{isExpanded && hasChildren && (
<div className="mt-1 rounded-r-lg w-full">
{childOptions.map((childOption, index) => (
<MenuOption
key={index}
{...childOption} // flex and roles go here.
user={user}
isChild={true}
/>
))}
</div>
)}
</div>
);
}
function useIsExpanded({
storageKey = "",
hasVisibleChildren = false,
childOptions = [],
location = null,
}) {
const [isExpanded, setIsExpanded] = useState(() => {
if (hasVisibleChildren) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== null) {
return JSON.parse(storedValue);
}
return childOptions.some((child) => child.href === location);
}
return false;
});
useEffect(() => {
if (hasVisibleChildren) {
const shouldExpand = childOptions.some(
(child) => child.href === location
);
if (shouldExpand && !isExpanded) {
setIsExpanded(true);
localStorage.setItem(storageKey, JSON.stringify(true));
}
}
}, [location]);
return { isExpanded, setIsExpanded };
}
function hasVisibleOptions(user = null, childOptions = []) {
if (!Array.isArray(childOptions) || childOptions?.length === 0) return false;
function isVisible({ roles = [], user = null, flex = false }) {
if (!flex && !roles.includes(user?.role)) return false;
if (flex && !!user && !roles.includes(user?.role)) return false;
return true;
}
return childOptions.some((opt) =>
isVisible({ roles: opt.roles, user, flex: opt.flex })
);
}
function generateStorageKey({ key = "" }) {
const _key = key.replace(/\s+/g, "_").toLowerCase();
return `anything_llm_menu_${_key}_expanded`;
}

View file

@ -2,28 +2,15 @@ import React, { useEffect, useRef, useState } from "react";
import paths from "@/utils/paths";
import useLogo from "@/hooks/useLogo";
import {
EnvelopeSimple,
SquaresFour,
Users,
BookOpen,
ChatCenteredText,
Eye,
Key,
ChatText,
Database,
Lock,
House,
List,
FileCode,
Notepad,
CodeBlock,
Barcode,
ClosedCaptioning,
EyeSlash,
SplitVertical,
Microphone,
Robot,
Flask,
Gear,
UserCircleGear,
PencilSimpleLine,
Nut,
Toolbox,
} from "@phosphor-icons/react";
import useUser from "@/hooks/useUser";
import { USER_BACKGROUND_COLOR } from "@/utils/constants";
@ -32,6 +19,8 @@ import Footer from "../Footer";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import showToast from "@/utils/toast";
import System from "@/models/system";
import Option from "./MenuOption";
export default function SettingsSidebar() {
const { t } = useTranslation();
@ -118,6 +107,17 @@ export default function SettingsSidebar() {
<div className="h-auto md:sidebar-items md:dark:sidebar-items">
<div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll">
<SidebarOptions user={user} t={t} />
<div className="h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]" />
<SupportEmail />
<Link
hidden={
user?.hasOwnProperty("role") && user.role !== "admin"
}
to={paths.settings.privacy()}
className="text-darker hover:text-white text-xs leading-[18px] mx-3"
>
Privacy & Data
</Link>
</div>
</div>
</div>
@ -156,6 +156,15 @@ export default function SettingsSidebar() {
<div className="h-auto sidebar-items">
<div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll">
<SidebarOptions user={user} t={t} />
<div className="h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]" />
<SupportEmail />
<Link
hidden={user?.hasOwnProperty("role") && user.role !== "admin"}
to={paths.settings.privacy()}
className="text-darker hover:text-white text-xs leading-[18px] mx-3"
>
Privacy & Data
</Link>
</div>
</div>
</div>
@ -168,233 +177,173 @@ export default function SettingsSidebar() {
);
}
const Option = ({
btnText,
icon,
href,
childLinks = [],
flex = false,
user = null,
allowedRole = [],
subOptions = null,
hidden = false,
}) => {
if (hidden) return null;
function SupportEmail() {
const [supportEmail, setSupportEmail] = useState(paths.mailToMintplex());
const hasActiveChild = childLinks.includes(window.location.pathname);
const isActive = window.location.pathname === href;
// Option only for multi-user
if (!flex && !allowedRole.includes(user?.role)) return null;
// Option is dual-mode, but user exists, we need to check permissions
if (flex && !!user && !allowedRole.includes(user?.role)) return null;
useEffect(() => {
const fetchSupportEmail = async () => {
const supportEmail = await System.fetchSupportEmail();
setSupportEmail(
supportEmail?.email
? `mailto:${supportEmail.email}`
: paths.mailToMintplex()
);
};
fetchSupportEmail();
}, []);
return (
<>
<div className="flex gap-x-2 items-center justify-between">
<Link
to={href}
className={`
transition-all duration-[200ms]
flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-[4px] justify-start items-center
hover:bg-workspace-item-selected-gradient hover:text-white hover:font-medium
${
isActive
? "bg-menu-item-selected-gradient font-medium border-outline text-white"
: "hover:bg-menu-item-selected-gradient text-zinc-200"
}
`}
>
{React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })}
<p className="text-sm leading-loose whitespace-nowrap overflow-hidden ">
{btnText}
</p>
</Link>
</div>
{!!subOptions && (isActive || hasActiveChild) && (
<div
className={`ml-4 ${
hasActiveChild ? "" : "border-l-2 border-slate-400"
} rounded-r-lg`}
>
{subOptions}
</div>
)}
</>
<Link
to={supportEmail}
className="text-darker hover:text-white text-xs leading-[18px] mx-3 mt-1"
>
Contact Support
</Link>
);
};
}
const SidebarOptions = ({ user = null, t }) => (
<>
<Option
href={paths.settings.system()}
btnText={t("settings.system")}
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
btnText={t("settings.ai-providers")}
icon={<Gear className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
childOptions={[
{
btnText: t("settings.llm"),
href: paths.settings.llmPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.vector-database"),
href: paths.settings.vectorDatabase(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embedder"),
href: paths.settings.embedder.modelPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.text-splitting"),
href: paths.settings.embedder.chunkingPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: "Voice & Speech",
href: paths.settings.audioPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.transcription"),
href: paths.settings.transcriptionPreference(),
flex: true,
roles: ["admin"],
},
]}
/>
<Option
href={paths.settings.invites()}
btnText={t("settings.invites")}
icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
btnText={t("settings.admin")}
icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
childOptions={[
{
btnText: t("settings.users"),
href: paths.settings.users(),
roles: ["admin", "manager"],
},
{
btnText: t("settings.workspaces"),
href: paths.settings.workspaces(),
roles: ["admin", "manager"],
},
{
btnText: t("settings.workspace-chats"),
href: paths.settings.chats(),
flex: true,
roles: ["admin", "manager"],
},
{
btnText: t("settings.invites"),
href: paths.settings.invites(),
roles: ["admin", "manager"],
},
{
btnText: t("settings.system"),
href: paths.settings.system(),
roles: ["admin", "manager"],
},
]}
/>
<Option
href={paths.settings.users()}
btnText={t("settings.users")}
icon={<Users className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.workspaces()}
btnText={t("settings.workspaces")}
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.chats()}
btnText={t("settings.workspace-chats")}
icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.agentSkills()}
btnText="Agent Skills"
btnText={t("settings.agent-skills")}
icon={<Robot className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.agentSkills()}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
roles={["admin"]}
/>
<Option
btnText={t("settings.customization")}
icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.appearance()}
btnText={t("settings.appearance")}
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
roles={["admin", "manager"]}
/>
<Option
href={paths.settings.apiKeys()}
btnText={t("settings.api-keys")}
icon={<Key className="h-5 w-5 flex-shrink-0" />}
btnText={t("settings.tools")}
icon={<Toolbox className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
childOptions={[
{
btnText: t("settings.embed-chats"),
href: paths.settings.embedChats(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embeds"),
href: paths.settings.embedSetup(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.event-logs"),
href: paths.settings.logs(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.api-keys"),
href: paths.settings.apiKeys(),
flex: true,
roles: ["admin"],
},
]}
/>
<Option
href={paths.settings.llmPreference()}
btnText={t("settings.llm")}
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.audioPreference()}
btnText="Voice and Speech Support"
icon={<Microphone className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.transcriptionPreference()}
btnText={t("settings.transcription")}
icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.embedder.modelPreference()}
childLinks={[paths.settings.embedder.chunkingPreference()]}
btnText={t("settings.embedder")}
icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
subOptions={
<>
<Option
href={paths.settings.embedder.chunkingPreference()}
btnText={t("settings.text-splitting")}
icon={<SplitVertical className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
</>
}
/>
<Option
href={paths.settings.vectorDatabase()}
btnText={t("settings.vector-database")}
icon={<Database className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.embedSetup()}
childLinks={[paths.settings.embedChats()]}
btnText={t("settings.embeds")}
icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
subOptions={
<>
<Option
href={paths.settings.embedChats()}
btnText={t("settings.embed-chats")}
icon={<Barcode className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
</>
}
/>
<Option
href={paths.settings.security()}
btnText={t("settings.security")}
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
icon={<Nut className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.security()}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
roles={["admin", "manager"]}
hidden={user?.role}
/>
<Option
href={paths.settings.logs()}
btnText={t("settings.event-logs")}
icon={<Notepad className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.privacy()}
btnText={t("settings.privacy")}
icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<HoldToReveal key="exp_features">
<Option
href={paths.settings.experimental()}
btnText="Experimental Features"
icon={<Flask className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.experimental()}
user={user}
flex={true}
allowedRole={["admin"]}
roles={["admin"]}
/>
</HoldToReveal>
</>

View file

@ -14,23 +14,27 @@ const TRANSLATIONS = {
// Setting Sidebar menu items.
settings: {
title: "Instance Settings",
system: "System Preferences",
invites: "Invitation",
system: "General Settings",
invites: "Invites",
users: "Users",
workspaces: "Workspaces",
"workspace-chats": "Workspace Chat",
appearance: "Appearance",
"api-keys": "API Keys",
llm: "LLM Preference",
transcription: "Transcription Model",
embedder: "Embedding Preferences",
"workspace-chats": "Workspace Chats",
customization: "Customization",
"api-keys": "Developer API",
llm: "LLM",
transcription: "Transcription",
embedder: "Embedder",
"text-splitting": "Text Splitter & Chunking",
"vector-database": "Vector Database",
embeds: "Chat Embed Widgets",
embeds: "Chat Embed",
"embed-chats": "Chat Embed History",
security: "Security",
"event-logs": "Event Logs",
privacy: "Privacy & Data",
"ai-providers": "AI Providers",
"agent-skills": "Agent Skills",
admin: "Admin",
tools: "Tools",
},
// Page Definitions

View file

@ -18,7 +18,7 @@ const TRANSLATIONS = {
users: "Usuarios",
workspaces: "Espacios de trabajo",
"workspace-chats": "Chat del espacio de trabajo",
appearance: "Apariencia",
customization: "Apariencia",
"api-keys": "Claves API",
llm: "Preferencia de LLM",
transcription: "Modelo de transcripción",
@ -30,6 +30,10 @@ const TRANSLATIONS = {
security: "Seguridad",
"event-logs": "Registros de eventos",
privacy: "Privacidad y datos",
"ai-providers": "Proveedores de IA",
"agent-skills": "Habilidades del agente",
admin: "Administrador",
tools: "Herramientas",
},
login: {

View file

@ -19,7 +19,7 @@ const TRANSLATIONS = {
users: "Utilisateurs",
workspaces: "Espaces de travail",
"workspace-chats": "Chat de l'espace de travail",
appearance: "Apparence",
customization: "Apparence",
"api-keys": "Clés API",
llm: "Préférence LLM",
transcription: "Modèle de transcription",
@ -31,6 +31,10 @@ const TRANSLATIONS = {
security: "Sécurité",
"event-logs": "Journaux d'événements",
privacy: "Confidentialité et données",
"ai-providers": "Fournisseurs d'IA",
"agent-skills": "Compétences de l'agent",
admin: "Admin",
tools: "Outils",
},
// Page Definitions

View file

@ -17,7 +17,7 @@ const TRANSLATIONS = {
users: "Пользователи",
workspaces: "Рабочие пространства",
"workspace-chats": "Чат рабочего пространства",
appearance: "Внешний вид",
customization: "Внешний вид",
"api-keys": "API ключи",
llm: "Предпочтение LLM",
transcription: "Модель транскрипции",
@ -29,6 +29,10 @@ const TRANSLATIONS = {
security: "Безопасность",
"event-logs": "Журналы событий",
privacy: "Конфиденциальность и данные",
"ai-providers": "Поставщики ИИ",
"agent-skills": "Навыки агента",
admin: "Администратор",
tools: "Инструменты",
},
login: {
"multi-user": {

View file

@ -20,7 +20,7 @@ const TRANSLATIONS = {
users: "用户",
workspaces: "工作区",
"workspace-chats": "对话历史记录", // "workspace-chats" should be "对话历史记录", means "chat history",or "chat history records"
appearance: "外观",
customization: "外观",
"api-keys": "API 密钥",
llm: "LLM 首选项",
transcription: "Transcription 模型",
@ -32,6 +32,10 @@ const TRANSLATIONS = {
security: "用户与安全",
"event-logs": "事件日志",
privacy: "隐私与数据",
"ai-providers": "人工智能提供商",
"agent-skills": "代理技能",
admin: "管理员",
tools: "工具",
},
// Page Definitions

View file

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'false',
darkMode: "false",
content: {
relative: true,
files: [
@ -11,7 +11,7 @@ export default {
"./src/utils/**/*.js",
"./src/*.jsx",
"./index.html",
'./node_modules/@tremor/**/*.{js,ts,jsx,tsx}'
"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}"
]
},
theme: {
@ -35,7 +35,8 @@ export default {
"dark-highlight": "#1C1E21",
"dark-text": "#222628",
description: "#D2D5DB",
"x-button": "#9CA3AF"
"x-button": "#9CA3AF",
darker: "#F4F4F4"
},
backgroundImage: {
"preference-gradient":
@ -101,30 +102,30 @@ export default {
{
pattern:
/^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
variants: ['hover', 'ui-selected'],
variants: ["hover", "ui-selected"]
},
{
pattern:
/^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
variants: ['hover', 'ui-selected'],
variants: ["hover", "ui-selected"]
},
{
pattern:
/^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
variants: ['hover', 'ui-selected'],
variants: ["hover", "ui-selected"]
},
{
pattern:
/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/
},
{
pattern:
/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/
},
{
pattern:
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
},
/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/
}
],
plugins: []
}