Add ability to control available pages

This commit is contained in:
Weves 2024-03-31 21:42:30 -07:00 committed by Chris Weaver
parent 15f7b42e2b
commit a4869b727d
17 changed files with 399 additions and 152 deletions

View File

@ -76,6 +76,8 @@ from danswer.server.query_and_chat.query_backend import (
admin_router as admin_query_router, admin_router as admin_query_router,
) )
from danswer.server.query_and_chat.query_backend import basic_router as query_router from danswer.server.query_and_chat.query_backend import basic_router as query_router
from danswer.server.settings.api import admin_router as settings_admin_router
from danswer.server.settings.api import basic_router as settings_router
from danswer.utils.logger import setup_logger from danswer.utils.logger import setup_logger
from danswer.utils.telemetry import optional_telemetry from danswer.utils.telemetry import optional_telemetry
from danswer.utils.telemetry import RecordType from danswer.utils.telemetry import RecordType
@ -279,6 +281,8 @@ def get_application() -> FastAPI:
include_router_with_global_prefix_prepended(application, state_router) include_router_with_global_prefix_prepended(application, state_router)
include_router_with_global_prefix_prepended(application, danswer_api_router) include_router_with_global_prefix_prepended(application, danswer_api_router)
include_router_with_global_prefix_prepended(application, gpts_router) include_router_with_global_prefix_prepended(application, gpts_router)
include_router_with_global_prefix_prepended(application, settings_router)
include_router_with_global_prefix_prepended(application, settings_admin_router)
if AUTH_TYPE == AuthType.DISABLED: if AUTH_TYPE == AuthType.DISABLED:
# Server logs this during auth setup verification step # Server logs this during auth setup verification step

View File

@ -0,0 +1,30 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from danswer.auth.users import current_admin_user
from danswer.auth.users import current_user
from danswer.db.models import User
from danswer.server.settings.models import Settings
from danswer.server.settings.store import load_settings
from danswer.server.settings.store import store_settings
admin_router = APIRouter(prefix="/admin/settings")
basic_router = APIRouter(prefix="/settings")
@admin_router.put("")
def put_settings(
settings: Settings, _: User | None = Depends(current_admin_user)
) -> None:
try:
settings.check_validity()
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
store_settings(settings)
@basic_router.get("")
def fetch_settings(_: User | None = Depends(current_user)) -> Settings:
return load_settings()

View File

@ -0,0 +1,36 @@
from enum import Enum
from pydantic import BaseModel
class PageType(str, Enum):
CHAT = "chat"
SEARCH = "search"
class Settings(BaseModel):
"""General settings"""
chat_page_enabled: bool = True
search_page_enabled: bool = True
default_page: PageType = PageType.SEARCH
def check_validity(self) -> None:
chat_page_enabled = self.chat_page_enabled
search_page_enabled = self.search_page_enabled
default_page = self.default_page
if chat_page_enabled is False and search_page_enabled is False:
raise ValueError(
"One of `search_page_enabled` and `chat_page_enabled` must be True."
)
if default_page == PageType.CHAT and chat_page_enabled is False:
raise ValueError(
"The default page cannot be 'chat' if the chat page is disabled."
)
if default_page == PageType.SEARCH and search_page_enabled is False:
raise ValueError(
"The default page cannot be 'search' if the search page is disabled."
)

View File

@ -0,0 +1,23 @@
from typing import cast
from danswer.dynamic_configs.factory import get_dynamic_config_store
from danswer.dynamic_configs.interface import ConfigNotFoundError
from danswer.server.settings.models import Settings
_SETTINGS_KEY = "danswer_settings"
def load_settings() -> Settings:
dynamic_config_store = get_dynamic_config_store()
try:
settings = Settings(**cast(dict, dynamic_config_store.load(_SETTINGS_KEY)))
except ConfigNotFoundError:
settings = Settings()
dynamic_config_store.store(_SETTINGS_KEY, settings.dict())
return settings
def store_settings(settings: Settings) -> None:
get_dynamic_config_store().store(_SETTINGS_KEY, settings.dict())

View File

@ -24,13 +24,7 @@ const nextConfig = {
// In production, something else (nginx in the one box setup) should take // In production, something else (nginx in the one box setup) should take
// care of this redirect. TODO (chris): better support setups where // care of this redirect. TODO (chris): better support setups where
// web_server and api_server are on different machines. // web_server and api_server are on different machines.
const defaultRedirects = [ const defaultRedirects = [];
{
source: "/",
destination: "/search",
permanent: true,
},
];
if (process.env.NODE_ENV === "production") return defaultRedirects; if (process.env.NODE_ENV === "production") return defaultRedirects;

View File

@ -0,0 +1,149 @@
"use client";
import { Label, SubLabel } from "@/components/admin/connectors/Field";
import { Title } from "@tremor/react";
import { Settings } from "./interfaces";
import { useRouter } from "next/navigation";
import { DefaultDropdown, Option } from "@/components/Dropdown";
function Checkbox({
label,
sublabel,
checked,
onChange,
}: {
label: string;
sublabel: string;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<label className="flex text-sm mb-4">
<input
checked={checked}
onChange={onChange}
type="checkbox"
className="mx-3 px-5 w-3.5 h-3.5 my-auto"
/>
<div>
<Label>{label}</Label>
<SubLabel>{sublabel}</SubLabel>
</div>
</label>
);
}
function Selector({
label,
subtext,
options,
selected,
onSelect,
}: {
label: string;
subtext: string;
options: Option<string>[];
selected: string;
onSelect: (value: string | number | null) => void;
}) {
return (
<div>
{label && <Label>{label}</Label>}
{subtext && <SubLabel>{subtext}</SubLabel>}
<div className="mt-2">
<DefaultDropdown
options={options}
selected={selected}
onSelect={onSelect}
/>
</div>
</div>
);
}
export function SettingsForm({ settings }: { settings: Settings }) {
const router = useRouter();
async function updateSettingField(
updateRequests: { fieldName: keyof Settings; newValue: any }[]
) {
const newValues: any = {};
updateRequests.forEach(({ fieldName, newValue }) => {
newValues[fieldName] = newValue;
});
const response = await fetch("/api/admin/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...settings,
...newValues,
}),
});
if (response.ok) {
router.refresh();
} else {
const errorMsg = (await response.json()).detail;
alert(`Failed to update settings. ${errorMsg}`);
}
}
return (
<div>
<Title className="mb-4">Page Visibility</Title>
<Checkbox
label="Search Page Enabled?"
sublabel={`If set, then the "Search" page will be accessible to all users
and will show up as an option on the top navbar. If unset, then this
page will not be available.`}
checked={settings.search_page_enabled}
onChange={(e) => {
const updates: any[] = [
{ fieldName: "search_page_enabled", newValue: e.target.checked },
];
if (!e.target.checked && settings.default_page === "search") {
updates.push({ fieldName: "default_page", newValue: "chat" });
}
updateSettingField(updates);
}}
/>
<Checkbox
label="Chat Page Enabled?"
sublabel={`If set, then the "Chat" page will be accessible to all users
and will show up as an option on the top navbar. If unset, then this
page will not be available.`}
checked={settings.chat_page_enabled}
onChange={(e) => {
const updates: any[] = [
{ fieldName: "chat_page_enabled", newValue: e.target.checked },
];
if (!e.target.checked && settings.default_page === "chat") {
updates.push({ fieldName: "default_page", newValue: "search" });
}
updateSettingField(updates);
}}
/>
<Selector
label="Default Page"
subtext="The page that users will be redirected to after logging in. Can only be set to a page that is enabled."
options={[
{ value: "search", name: "Search" },
{ value: "chat", name: "Chat" },
]}
selected={settings.default_page}
onSelect={(value) => {
value &&
updateSettingField([
{ fieldName: "default_page", newValue: value },
]);
}}
/>
</div>
);
}

View File

@ -0,0 +1,5 @@
export interface Settings {
chat_page_enabled: boolean;
search_page_enabled: boolean;
default_page: "search" | "chat";
}

View File

@ -0,0 +1,33 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { FiSettings } from "react-icons/fi";
import { Settings } from "./interfaces";
import { fetchSS } from "@/lib/utilsSS";
import { SettingsForm } from "./SettingsForm";
import { Callout, Text } from "@tremor/react";
export default async function Page() {
const response = await fetchSS("/settings");
if (!response.ok) {
const errorMsg = await response.text();
return <Callout title="Failed to fetch settings">{errorMsg}</Callout>;
}
const settings = (await response.json()) as Settings;
return (
<div className="mx-auto container">
<AdminPageTitle
title="Workspace Settings"
icon={<FiSettings size={32} className="my-auto" />}
/>
<Text className="mb-8">
Manage general Danswer settings applicable to all users in the
workspace.
</Text>
<SettingsForm settings={settings} />
</div>
);
}

View File

@ -9,9 +9,11 @@ import { Persona } from "../admin/personas/interfaces";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { HealthCheckBanner } from "@/components/health/healthcheck"; import { HealthCheckBanner } from "@/components/health/healthcheck";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { Settings } from "../admin/settings/interfaces";
export function ChatLayout({ export function ChatLayout({
user, user,
settings,
chatSessions, chatSessions,
availableSources, availableSources,
availableDocumentSets, availableDocumentSets,
@ -21,6 +23,7 @@ export function ChatLayout({
documentSidebarInitialWidth, documentSidebarInitialWidth,
}: { }: {
user: User | null; user: User | null;
settings: Settings | null;
chatSessions: ChatSession[]; chatSessions: ChatSession[];
availableSources: ValidSources[]; availableSources: ValidSources[];
availableDocumentSets: DocumentSet[]; availableDocumentSets: DocumentSet[];
@ -40,7 +43,7 @@ export function ChatLayout({
return ( return (
<> <>
<div className="absolute top-0 z-40 w-full"> <div className="absolute top-0 z-40 w-full">
<Header user={user} /> <Header user={user} settings={settings} />
</div> </div>
<HealthCheckBanner /> <HealthCheckBanner />
<InstantSSRAutoRefresh /> <InstantSSRAutoRefresh />

View File

@ -27,6 +27,8 @@ import { personaComparator } from "../admin/personas/lib";
import { ChatLayout } from "./ChatPage"; import { ChatLayout } from "./ChatPage";
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels"; import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal"; import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
import { getSettingsSS } from "@/lib/settings";
import { Settings } from "../admin/settings/interfaces";
export default async function Page({ export default async function Page({
searchParams, searchParams,
@ -43,6 +45,7 @@ export default async function Page({
fetchSS("/persona?include_default=true"), fetchSS("/persona?include_default=true"),
fetchSS("/chat/get-user-chat-sessions"), fetchSS("/chat/get-user-chat-sessions"),
fetchSS("/query/valid-tags"), fetchSS("/query/valid-tags"),
getSettingsSS(),
]; ];
// catch cases where the backend is completely unreachable here // catch cases where the backend is completely unreachable here
@ -53,8 +56,9 @@ export default async function Page({
| Response | Response
| AuthTypeMetadata | AuthTypeMetadata
| FullEmbeddingModelResponse | FullEmbeddingModelResponse
| Settings
| null | null
)[] = [null, null, null, null, null, null, null, null, null]; )[] = [null, null, null, null, null, null, null, null, null, null];
try { try {
results = await Promise.all(tasks); results = await Promise.all(tasks);
} catch (e) { } catch (e) {
@ -67,6 +71,7 @@ export default async function Page({
const personasResponse = results[4] as Response | null; const personasResponse = results[4] as Response | null;
const chatSessionsResponse = results[5] as Response | null; const chatSessionsResponse = results[5] as Response | null;
const tagsResponse = results[6] as Response | null; const tagsResponse = results[6] as Response | null;
const settings = results[7] as Settings | null;
const authDisabled = authTypeMetadata?.authType === "disabled"; const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) { if (!authDisabled && !user) {
@ -77,6 +82,10 @@ export default async function Page({
return redirect("/auth/waiting-on-verification"); return redirect("/auth/waiting-on-verification");
} }
if (settings && !settings.chat_page_enabled) {
return redirect("/search");
}
let ccPairs: CCPairBasicInfo[] = []; let ccPairs: CCPairBasicInfo[] = [];
if (ccPairsResponse?.ok) { if (ccPairsResponse?.ok) {
ccPairs = await ccPairsResponse.json(); ccPairs = await ccPairsResponse.json();
@ -172,6 +181,7 @@ export default async function Page({
<ChatLayout <ChatLayout
user={user} user={user}
settings={settings}
chatSessions={chatSessions} chatSessions={chatSessions}
availableSources={availableSources} availableSources={availableSources}
availableDocumentSets={documentSets} availableDocumentSets={documentSets}

View File

@ -9,6 +9,8 @@ import { redirect } from "next/navigation";
import { BackendChatSession } from "../../interfaces"; import { BackendChatSession } from "../../interfaces";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { SharedChatDisplay } from "./SharedChatDisplay"; import { SharedChatDisplay } from "./SharedChatDisplay";
import { getSettingsSS } from "@/lib/settings";
import { Settings } from "@/app/admin/settings/interfaces";
async function getSharedChat(chatId: string) { async function getSharedChat(chatId: string) {
const response = await fetchSS( const response = await fetchSS(
@ -25,22 +27,13 @@ export default async function Page({ params }: { params: { chatId: string } }) {
getAuthTypeMetadataSS(), getAuthTypeMetadataSS(),
getCurrentUserSS(), getCurrentUserSS(),
getSharedChat(params.chatId), getSharedChat(params.chatId),
getSettingsSS(),
]; ];
// catch cases where the backend is completely unreachable here // catch cases where the backend is completely unreachable here
// without try / catch, will just raise an exception and the page // without try / catch, will just raise an exception and the page
// will not render // will not render
let results: (User | AuthTypeMetadata | null)[] = [ let results: (User | AuthTypeMetadata | null)[] = [null, null, null, null];
null,
null,
null,
null,
null,
null,
null,
null,
null,
];
try { try {
results = await Promise.all(tasks); results = await Promise.all(tasks);
} catch (e) { } catch (e) {
@ -49,6 +42,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
const authTypeMetadata = results[0] as AuthTypeMetadata | null; const authTypeMetadata = results[0] as AuthTypeMetadata | null;
const user = results[1] as User | null; const user = results[1] as User | null;
const chatSession = results[2] as BackendChatSession | null; const chatSession = results[2] as BackendChatSession | null;
const settings = results[3] as Settings | null;
const authDisabled = authTypeMetadata?.authType === "disabled"; const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) { if (!authDisabled && !user) {
@ -62,7 +56,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
return ( return (
<div> <div>
<div className="absolute top-0 z-40 w-full"> <div className="absolute top-0 z-40 w-full">
<Header user={user} /> <Header user={user} settings={settings} />
</div> </div>
<div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen"> <div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen">

16
web/src/app/page.tsx Normal file
View File

@ -0,0 +1,16 @@
import { getSettingsSS } from "@/lib/settings";
import { redirect } from "next/navigation";
export default async function Page() {
const settings = await getSettingsSS();
if (!settings) {
redirect("/search");
}
if (settings.default_page === "search") {
redirect("/search");
} else {
redirect("/chat");
}
}

View File

@ -23,6 +23,8 @@ import { personaComparator } from "../admin/personas/lib";
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels"; import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal"; import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal"; import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
import { getSettingsSS } from "@/lib/settings";
import { Settings } from "../admin/settings/interfaces";
export default async function Home() { export default async function Home() {
// Disable caching so we always get the up to date connector / document set / persona info // Disable caching so we always get the up to date connector / document set / persona info
@ -38,6 +40,7 @@ export default async function Home() {
fetchSS("/persona"), fetchSS("/persona"),
fetchSS("/query/valid-tags"), fetchSS("/query/valid-tags"),
fetchSS("/secondary-index/get-embedding-models"), fetchSS("/secondary-index/get-embedding-models"),
getSettingsSS(),
]; ];
// catch cases where the backend is completely unreachable here // catch cases where the backend is completely unreachable here
@ -48,6 +51,7 @@ export default async function Home() {
| Response | Response
| AuthTypeMetadata | AuthTypeMetadata
| FullEmbeddingModelResponse | FullEmbeddingModelResponse
| Settings
| null | null
)[] = [null, null, null, null, null, null, null]; )[] = [null, null, null, null, null, null, null];
try { try {
@ -62,6 +66,7 @@ export default async function Home() {
const personaResponse = results[4] as Response | null; const personaResponse = results[4] as Response | null;
const tagsResponse = results[5] as Response | null; const tagsResponse = results[5] as Response | null;
const embeddingModelResponse = results[6] as Response | null; const embeddingModelResponse = results[6] as Response | null;
const settings = results[7] as Settings | null;
const authDisabled = authTypeMetadata?.authType === "disabled"; const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) { if (!authDisabled && !user) {
@ -72,6 +77,10 @@ export default async function Home() {
return redirect("/auth/waiting-on-verification"); return redirect("/auth/waiting-on-verification");
} }
if (settings && !settings.search_page_enabled) {
return redirect("/chat");
}
let ccPairs: CCPairBasicInfo[] = []; let ccPairs: CCPairBasicInfo[] = [];
if (ccPairsResponse?.ok) { if (ccPairsResponse?.ok) {
ccPairs = await ccPairsResponse.json(); ccPairs = await ccPairsResponse.json();
@ -143,7 +152,7 @@ export default async function Home() {
return ( return (
<> <>
<Header user={user} /> <Header user={user} settings={settings} />
<div className="m-3"> <div className="m-3">
<HealthCheckBanner /> <HealthCheckBanner />
</div> </div>

View File

@ -1,7 +1,6 @@
import { ChangeEvent, FC, useEffect, useRef, useState } from "react"; import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
import { ChevronDownIcon } from "./icons/icons"; import { ChevronDownIcon } from "./icons/icons";
import { FiCheck, FiChevronDown } from "react-icons/fi"; import { FiCheck, FiChevronDown } from "react-icons/fi";
import { FaRobot } from "react-icons/fa";
export interface Option<T> { export interface Option<T> {
name: string; name: string;
@ -12,108 +11,6 @@ export interface Option<T> {
export type StringOrNumberOption = Option<string | number>; export type StringOrNumberOption = Option<string | number>;
interface DropdownProps<T> {
options: Option<T>[];
selected: string;
onSelect: (selected: Option<T> | null) => void;
}
export const Dropdown = ({
options,
selected,
onSelect,
}: DropdownProps<string | number>) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const selectedName = options.find(
(option) => option.value === selected
)?.name;
const handleSelect = (option: StringOrNumberOption) => {
onSelect(option);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
<div>
<button
type="button"
className={`inline-flex
justify-center
w-full
px-4
py-3
text-sm
bg-gray-700
border
border-gray-300
rounded-md
shadow-sm
hover:bg-gray-700
focus:ring focus:ring-offset-0 focus:ring-1 focus:ring-offset-gray-800 focus:ring-blue-800
`}
id="options-menu"
aria-expanded="true"
aria-haspopup="true"
onClick={() => setIsOpen(!isOpen)}
>
{selectedName ? <p>{selectedName}</p> : "Select an option..."}
<ChevronDownIcon className="text-gray-400 my-auto ml-auto" />
</button>
</div>
{isOpen ? (
<div className="origin-top-right absolute left-0 mt-3 w-full rounded-md shadow-lg bg-gray-700 border-2 border-gray-600">
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
{options.map((option, index) => (
<button
key={index}
onClick={() => handleSelect(option)}
className={
`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800` +
(index !== 0 ? " border-t-2 border-gray-600" : "")
}
role="menuitem"
>
<p className="font-medium">{option.name}</p>
{option.description && (
<div>
<p className="text-xs text-gray-300">
{option.description}
</p>
</div>
)}
</button>
))}
</div>
</div>
) : null}
</div>
);
};
function StandardDropdownOption<T>({ function StandardDropdownOption<T>({
index, index,
option, option,

View File

@ -9,14 +9,15 @@ import React, { useEffect, useRef, useState } from "react";
import { CustomDropdown, DefaultDropdownElement } from "./Dropdown"; import { CustomDropdown, DefaultDropdownElement } from "./Dropdown";
import { FiMessageSquare, FiSearch } from "react-icons/fi"; import { FiMessageSquare, FiSearch } from "react-icons/fi";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Settings } from "@/app/admin/settings/interfaces";
interface HeaderProps { interface HeaderProps {
user: User | null; user: User | null;
settings: Settings | null;
} }
export const Header: React.FC<HeaderProps> = ({ user }) => { export function Header({ user, settings }: HeaderProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
@ -56,7 +57,12 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
return ( return (
<header className="border-b border-border bg-background-emphasis"> <header className="border-b border-border bg-background-emphasis">
<div className="mx-8 flex h-16"> <div className="mx-8 flex h-16">
<Link className="py-4" href="/search"> <Link
className="py-4"
href={
settings && settings.default_page === "chat" ? "/chat" : "/search"
}
>
<div className="flex"> <div className="flex">
<div className="h-[32px] w-[30px]"> <div className="h-[32px] w-[30px]">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" /> <Image src="/logo.png" alt="Logo" width="1419" height="1520" />
@ -67,26 +73,31 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
</div> </div>
</Link> </Link>
<Link {(!settings ||
href="/search" (settings.search_page_enabled && settings.chat_page_enabled)) && (
className={"ml-6 h-full flex flex-col hover:bg-hover"} <>
> <Link
<div className="w-24 flex my-auto"> href="/search"
<div className={"mx-auto flex text-strong px-2"}> className={"ml-6 h-full flex flex-col hover:bg-hover"}
<FiSearch className="my-auto mr-1" /> >
<h1 className="flex text-sm font-bold my-auto">Search</h1> <div className="w-24 flex my-auto">
</div> <div className={"mx-auto flex text-strong px-2"}>
</div> <FiSearch className="my-auto mr-1" />
</Link> <h1 className="flex text-sm font-bold my-auto">Search</h1>
</div>
</div>
</Link>
<Link href="/chat" className="h-full flex flex-col hover:bg-hover"> <Link href="/chat" className="h-full flex flex-col hover:bg-hover">
<div className="w-24 flex my-auto"> <div className="w-24 flex my-auto">
<div className="mx-auto flex text-strong px-2"> <div className="mx-auto flex text-strong px-2">
<FiMessageSquare className="my-auto mr-1" /> <FiMessageSquare className="my-auto mr-1" />
<h1 className="flex text-sm font-bold my-auto">Chat</h1> <h1 className="flex text-sm font-bold my-auto">Chat</h1>
</div> </div>
</div> </div>
</Link> </Link>
</>
)}
<div className="ml-auto h-full flex flex-col"> <div className="ml-auto h-full flex flex-col">
<div className="my-auto"> <div className="my-auto">
@ -124,7 +135,7 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
</div> </div>
</header> </header>
); );
}; }
/* /*

View File

@ -1,3 +1,4 @@
import { Settings } from "@/app/admin/settings/interfaces";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar"; import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
import { import {
@ -12,6 +13,7 @@ import {
ConnectorIcon, ConnectorIcon,
SlackIcon, SlackIcon,
} from "@/components/icons/icons"; } from "@/components/icons/icons";
import { getSettingsSS } from "@/lib/settings";
import { User } from "@/lib/types"; import { User } from "@/lib/types";
import { import {
AuthTypeMetadata, AuthTypeMetadata,
@ -19,15 +21,21 @@ import {
getCurrentUserSS, getCurrentUserSS,
} from "@/lib/userSS"; } from "@/lib/userSS";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { FiCpu, FiLayers, FiPackage, FiSlack } from "react-icons/fi"; import {
FiCpu,
FiLayers,
FiPackage,
FiSettings,
FiSlack,
} from "react-icons/fi";
export async function Layout({ children }: { children: React.ReactNode }) { export async function Layout({ children }: { children: React.ReactNode }) {
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()]; const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS(), getSettingsSS()];
// catch cases where the backend is completely unreachable here // catch cases where the backend is completely unreachable here
// without try / catch, will just raise an exception and the page // without try / catch, will just raise an exception and the page
// will not render // will not render
let results: (User | AuthTypeMetadata | null)[] = [null, null]; let results: (User | AuthTypeMetadata | Settings | null)[] = [null, null];
try { try {
results = await Promise.all(tasks); results = await Promise.all(tasks);
} catch (e) { } catch (e) {
@ -36,6 +44,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
const authTypeMetadata = results[0] as AuthTypeMetadata | null; const authTypeMetadata = results[0] as AuthTypeMetadata | null;
const user = results[1] as User | null; const user = results[1] as User | null;
const settings = results[2] as Settings | null;
const authDisabled = authTypeMetadata?.authType === "disabled"; const authDisabled = authTypeMetadata?.authType === "disabled";
const requiresVerification = authTypeMetadata?.requiresVerification; const requiresVerification = authTypeMetadata?.requiresVerification;
@ -54,7 +63,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="h-screen overflow-y-hidden"> <div className="h-screen overflow-y-hidden">
<div className="absolute top-0 z-50 w-full"> <div className="absolute top-0 z-50 w-full">
<Header user={user} /> <Header user={user} settings={settings} />
</div> </div>
<div className="flex h-full pt-16"> <div className="flex h-full pt-16">
<div className="w-80 pt-12 pb-8 h-full border-r border-border"> <div className="w-80 pt-12 pb-8 h-full border-r border-border">
@ -175,6 +184,20 @@ export async function Layout({ children }: { children: React.ReactNode }) {
}, },
], ],
}, },
{
name: "Settings",
items: [
{
name: (
<div className="flex">
<FiSettings size={18} />
<div className="ml-1">Workspace Settings</div>
</div>
),
link: "/admin/settings",
},
],
},
]} ]}
/> />
</div> </div>

10
web/src/lib/settings.ts Normal file
View File

@ -0,0 +1,10 @@
import { Settings } from "@/app/admin/settings/interfaces";
import { buildUrl } from "./utilsSS";
export async function getSettingsSS(): Promise<Settings | null> {
const response = await fetch(buildUrl("/settings"));
if (response.ok) {
return await response.json();
}
return null;
}