mirror of
https://github.com/Mintplex-Labs/vector-admin.git
synced 2026-07-19 13:16:03 -04:00
Merge pull request #96 from Mintplex-Labs/v2-user-menu-notifications
v2 user menu notifications
This commit is contained in:
@@ -1,195 +0,0 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { AlertOctagon, AlertTriangle, Bell, Info } from 'react-feather';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Organization from '../../../models/organization';
|
||||
import { databaseTimestampFromNow } from '../../../utils/data';
|
||||
import ChromaLogo from '../../../images/vectordbs/chroma.png';
|
||||
import PineconeLogo from '../../../images/vectordbs/pinecone.png';
|
||||
import qDrantLogo from '../../../images/vectordbs/qdrant.png';
|
||||
import WeaviateLogo from '../../../images/vectordbs/weaviate.png';
|
||||
|
||||
const POLLING_INTERVAL = 30_000;
|
||||
|
||||
export type INotification = {
|
||||
id: number;
|
||||
organization_id: number;
|
||||
seen: boolean;
|
||||
textContent: string;
|
||||
symbol?:
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'error'
|
||||
| 'chroma'
|
||||
| 'pinecone'
|
||||
| 'weaviate'
|
||||
| 'qdrant';
|
||||
link?: string;
|
||||
target?: '_blank' | 'self';
|
||||
createdAt: string;
|
||||
lastUpdatedAt: string;
|
||||
};
|
||||
|
||||
export default function Notifications() {
|
||||
const { slug } = useParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notifications, setNotifications] = useState<INotification[]>([]);
|
||||
const [hasUnseen, setHasUnseen] = useState(false);
|
||||
const [showNotifs, setShowNotifs] = useState(false);
|
||||
|
||||
async function handleClick() {
|
||||
if (!showNotifs) {
|
||||
!!slug && Organization.markNotificationsSeen(slug);
|
||||
setShowNotifs(true);
|
||||
setHasUnseen(false);
|
||||
} else {
|
||||
setShowNotifs(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { notifications: _notifications } = await Organization.notifications(
|
||||
slug
|
||||
);
|
||||
setNotifications(_notifications);
|
||||
setHasUnseen(_notifications.some((notif) => notif.seen === false));
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
fetchNotifications();
|
||||
setInterval(() => {
|
||||
fetchNotifications();
|
||||
}, POLLING_INTERVAL);
|
||||
}, [slug]);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="group rounded-lg px-4 py-2 text-slate-800 hover:bg-slate-200"
|
||||
>
|
||||
<div className="relative">
|
||||
<p
|
||||
hidden={!hasUnseen}
|
||||
className="absolute -top-[4px] right-0 h-[12px] w-[12px] rounded-full bg-red-600"
|
||||
/>
|
||||
<Bell
|
||||
size={20}
|
||||
className="group-hover:fill-blue-400 group-hover:stroke-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
hidden={!showNotifs}
|
||||
className="absolute right-0 top-10 z-99 max-h-[50vh] w-[20rem] divide-y divide-gray-100 overflow-y-scroll rounded-lg bg-white shadow"
|
||||
>
|
||||
<div className="block rounded-t-lg bg-blue-100/50 bg-gray-50 px-4 py-2 text-center font-medium text-blue-700">
|
||||
Recent Notifications
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="flex px-4 py-3 hover:bg-gray-100">
|
||||
<div className="w-full pl-3 text-center">
|
||||
<div className="mb-1.5 text-xs text-gray-500">
|
||||
no notifications
|
||||
</div>
|
||||
<div className="text-xs text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{notifications.map((notification) => (
|
||||
<Notification
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationImage({ notification }: { notification: INotification }) {
|
||||
switch (notification.symbol) {
|
||||
case 'info':
|
||||
return <Info className="text-blue-500" size={20} />;
|
||||
case 'warning':
|
||||
return <AlertTriangle className="text-orange-500" size={20} />;
|
||||
case 'error':
|
||||
return <AlertOctagon className="text-red-600" size={20} />;
|
||||
case 'chroma':
|
||||
return <img className="h-10 w-10 rounded-full" src={ChromaLogo} />;
|
||||
case 'pinecone':
|
||||
return <img className="h-8 w-8 rounded-full" src={PineconeLogo} />;
|
||||
case 'qdrant':
|
||||
return <img className="h-8 w-8 rounded-full" src={qDrantLogo} />;
|
||||
case 'weaviate':
|
||||
return <img className="h-8 w-8 rounded-full" src={WeaviateLogo} />;
|
||||
default:
|
||||
return <Info className="text-blue-500" size={20} />;
|
||||
}
|
||||
}
|
||||
|
||||
function NotificationWrapper({
|
||||
notification,
|
||||
children,
|
||||
}: {
|
||||
notification: INotification;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (!!notification.link) {
|
||||
return (
|
||||
<a
|
||||
key={notification.id}
|
||||
href={notification?.link || '#'}
|
||||
target={notification?.target || 'self'}
|
||||
className={`flex px-4 py-3 hover:bg-gray-100 ${
|
||||
!notification.seen ? 'border-l-4 !border-l-blue-600' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`flex px-4 py-3 hover:bg-gray-100 ${
|
||||
!notification.seen ? 'border-l-4 !border-l-blue-600' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Notification({ notification }: { notification: INotification }) {
|
||||
return (
|
||||
<NotificationWrapper notification={notification}>
|
||||
<div className="flex flex-shrink-0 items-center justify-center">
|
||||
<NotificationImage notification={notification} />
|
||||
</div>
|
||||
<div className="w-full pl-3">
|
||||
<div className="mb-1.5 text-sm text-gray-500">
|
||||
{notification.textContent}
|
||||
</div>
|
||||
<div className="text-xs text-blue-600">
|
||||
{databaseTimestampFromNow(notification.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Organization from '../../models/organization';
|
||||
import { databaseTimestampFromNow } from '../../utils/data';
|
||||
import ChromaLogo from '../../images/vectordbs/chroma.png';
|
||||
import PineconeLogo from '../../images/vectordbs/pinecone-inverted.png';
|
||||
import qDrantLogo from '../../images/vectordbs/qdrant.png';
|
||||
import WeaviateLogo from '../../images/vectordbs/weaviate.png';
|
||||
import { Bell, Info, Warning, WarningOctagon } from '@phosphor-icons/react';
|
||||
|
||||
const POLLING_INTERVAL = 30_000;
|
||||
|
||||
export type INotification = {
|
||||
id: number;
|
||||
organization_id: number;
|
||||
seen: boolean;
|
||||
textContent: string;
|
||||
symbol?:
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'error'
|
||||
| 'chroma'
|
||||
| 'pinecone'
|
||||
| 'weaviate'
|
||||
| 'qdrant';
|
||||
link?: string;
|
||||
target?: '_blank' | 'self';
|
||||
createdAt: string;
|
||||
lastUpdatedAt: string;
|
||||
};
|
||||
|
||||
export default function Notifications() {
|
||||
const { slug } = useParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notifications, setNotifications] = useState<INotification[]>([]);
|
||||
const [hasUnseen, setHasUnseen] = useState(false);
|
||||
const [showNotifs, setShowNotifs] = useState(false);
|
||||
const notificationRef = useRef(null);
|
||||
const bellButtonRef = useRef(null);
|
||||
|
||||
async function handleClick() {
|
||||
if (!showNotifs) {
|
||||
!!slug && Organization.markNotificationsSeen(slug);
|
||||
setShowNotifs(true);
|
||||
setHasUnseen(false);
|
||||
} else {
|
||||
setShowNotifs(false);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: any) {
|
||||
if (
|
||||
bellButtonRef.current &&
|
||||
bellButtonRef.current.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
notificationRef.current &&
|
||||
!notificationRef.current.contains(event.target)
|
||||
) {
|
||||
setShowNotifs(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function fetchNotifications() {
|
||||
if (!slug) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { notifications: _notifications } = await Organization.notifications(
|
||||
slug
|
||||
);
|
||||
setNotifications(_notifications);
|
||||
setHasUnseen(_notifications.some((notif) => notif.seen === false));
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
fetchNotifications();
|
||||
setInterval(() => {
|
||||
fetchNotifications();
|
||||
}, POLLING_INTERVAL);
|
||||
}, [slug]);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={bellButtonRef}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={`group rounded-lg p-2 hover:bg-main-2 ${
|
||||
showNotifs && 'bg-main-2'
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<p
|
||||
hidden={!hasUnseen}
|
||||
className="absolute -top-[4px] right-0 h-[12px] w-[12px] rounded-full bg-red-600"
|
||||
/>
|
||||
<div className="text-sky-400">
|
||||
<Bell size={24} weight={showNotifs ? 'fill' : 'bold'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
hidden={!showNotifs}
|
||||
ref={notificationRef}
|
||||
className="absolute right-0 top-12 z-1 max-h-[50vh] w-[20rem] overflow-y-auto rounded-lg border border-neutral-600 bg-main shadow-2xl"
|
||||
>
|
||||
<div className="sticky left-0 top-0 z-10 block rounded-t-lg border-b border-neutral-600 bg-main px-5 py-2 text-lg text-white shadow-lg">
|
||||
Notifications
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="flex px-4 py-3 hover:bg-main-2">
|
||||
<div className="w-full pl-3 text-center">
|
||||
<div className="mb-1.5 text-xs text-white">
|
||||
No notifications
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{notifications.map((notification) => (
|
||||
<Notification
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationImage({ notification }: { notification: INotification }) {
|
||||
switch (notification.symbol) {
|
||||
case 'info':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-2 text-white">
|
||||
<Info size={24} weight="bold" />
|
||||
</div>
|
||||
);
|
||||
case 'warning':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-yellow-300 bg-opacity-20 p-2 text-yellow-300">
|
||||
<WarningOctagon size={24} weight="bold" />
|
||||
</div>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-red-700/20 p-2 text-red-600">
|
||||
<Warning size={24} weight="bold" />
|
||||
</div>
|
||||
);
|
||||
case 'chroma':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
|
||||
<img alt="Chroma Logo" className="rounded-full" src={ChromaLogo} />
|
||||
</div>
|
||||
);
|
||||
case 'pinecone':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
|
||||
<img
|
||||
alt="Pinecone Logo"
|
||||
className="rounded-full"
|
||||
src={PineconeLogo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'qdrant':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
|
||||
<img alt="qDrant Logo" className="rounded-full" src={qDrantLogo} />
|
||||
</div>
|
||||
);
|
||||
case 'weaviate':
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
|
||||
<img
|
||||
alt="Weaviate Logo"
|
||||
className="rounded-full"
|
||||
src={WeaviateLogo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-2 text-white">
|
||||
<Info size={24} weight="bold" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function NotificationWrapper({
|
||||
notification,
|
||||
children,
|
||||
}: {
|
||||
notification: INotification;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (!!notification.link) {
|
||||
return (
|
||||
<a
|
||||
key={notification.id}
|
||||
href={notification?.link || '#'}
|
||||
target={notification?.target || 'self'}
|
||||
className={`flex px-4 py-3 transition-all duration-300 hover:bg-main-2 ${
|
||||
!notification.seen
|
||||
? 'border-l-2 !border-l-sky-400 bg-sky-400/10'
|
||||
: 'border-l-2 !border-l-transparent'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`flex px-4 py-3 hover:bg-main-2 ${
|
||||
!notification.seen
|
||||
? 'border-l-2 !border-l-sky-400'
|
||||
: 'border-l-2 !border-l-transparent'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Notification({ notification }: { notification: INotification }) {
|
||||
return (
|
||||
<NotificationWrapper notification={notification}>
|
||||
<div className="flex flex-shrink-0 items-center justify-center">
|
||||
<NotificationImage notification={notification} />
|
||||
</div>
|
||||
<div className="w-full pl-3">
|
||||
<div className="mb-1 text-sm font-medium leading-tight text-white">
|
||||
{notification.textContent}
|
||||
</div>
|
||||
<div className="text-sm text-white text-opacity-60">
|
||||
{databaseTimestampFromNow(notification.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// <Notification
|
||||
// key={'pinecone'}
|
||||
// notification={{
|
||||
// id: 1,
|
||||
// organization_id: 1,
|
||||
// seen: false,
|
||||
// textContent: 'Pinecone is now available!',
|
||||
// symbol: 'pinecone',
|
||||
// link: 'https://pinecone.io',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
// <Notification
|
||||
// key={'chroma'}
|
||||
// notification={{
|
||||
// id: 2,
|
||||
// organization_id: 1,
|
||||
// seen: false,
|
||||
// textContent: 'Chroma is now available!',
|
||||
// symbol: 'chroma',
|
||||
// link: 'https://chroma.ml',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
// <Notification
|
||||
// key={'qdrant'}
|
||||
// notification={{
|
||||
// id: 3,
|
||||
// organization_id: 1,
|
||||
// seen: false,
|
||||
// textContent: 'qDrant is now available!',
|
||||
// symbol: 'qdrant',
|
||||
// link: 'https://qdrant.tech',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
// <Notification
|
||||
// key={'weaviate'}
|
||||
// notification={{
|
||||
// id: 4,
|
||||
// organization_id: 1,
|
||||
// seen: false,
|
||||
// textContent: 'Weaviate is now available!',
|
||||
// symbol: 'weaviate',
|
||||
// link: 'https://weaviate.com',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
// <Notification
|
||||
// key={'error'}
|
||||
// notification={{
|
||||
// id: 5,
|
||||
// organization_id: 1,
|
||||
// seen: true,
|
||||
// textContent: 'Something went wrong!',
|
||||
// symbol: 'error',
|
||||
// link: 'https://weaviate.com',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
// <Notification
|
||||
// key={'warning'}
|
||||
// notification={{
|
||||
// id: 6,
|
||||
// organization_id: 1,
|
||||
// seen: true,
|
||||
// textContent: 'Something went wrong!',
|
||||
// symbol: 'warning',
|
||||
// link: 'https://weaviate.com',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
// <Notification
|
||||
// key={'info'}
|
||||
// notification={{
|
||||
// id: 7,
|
||||
// organization_id: 1,
|
||||
// seen: false,
|
||||
// textContent: 'Something went wrong!',
|
||||
// symbol: 'info',
|
||||
// link: 'https://weaviate.com',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />{' '}
|
||||
// <Notification
|
||||
// key={'info'}
|
||||
// notification={{
|
||||
// id: 7,
|
||||
// organization_id: 1,
|
||||
// seen: true,
|
||||
// textContent: 'Something went wrong!',
|
||||
// symbol: 'info',
|
||||
// link: 'https://weaviate.com',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />{' '}
|
||||
// <Notification
|
||||
// key={'info'}
|
||||
// notification={{
|
||||
// id: 7,
|
||||
// organization_id: 1,
|
||||
// seen: false,
|
||||
// textContent: 'Something went wrong!',
|
||||
// symbol: 'info',
|
||||
// link: 'https://weaviate.com',
|
||||
// target: '_blank',
|
||||
// createdAt: '2021-10-12T12:00:00Z',
|
||||
// lastUpdatedAt: '2021-10-12T12:00:00Z',
|
||||
// }}
|
||||
// />
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import useUser from '../../hooks/useUser';
|
||||
import paths from '../../utils/paths';
|
||||
import { STORE_TOKEN, STORE_USER } from '../../utils/constants';
|
||||
|
||||
export default function UserMenu() {
|
||||
const { user } = useUser();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef(null);
|
||||
const buttonRef = useRef();
|
||||
const handleClose = (event: any) => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(event.target) &&
|
||||
!buttonRef.current.contains(event.target)
|
||||
) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showMenu) {
|
||||
document.addEventListener('mousedown', handleClose);
|
||||
}
|
||||
return () => document.removeEventListener('mousedown', handleClose);
|
||||
}, [showMenu]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className="flex h-[29px] w-[29px] items-center justify-center rounded-full bg-sky-400 bg-opacity-20 text-sm font-medium text-sky-400"
|
||||
>
|
||||
{user?.email?.slice(0, 2).toUpperCase()}
|
||||
</button>
|
||||
{showMenu && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="items-center-justify-center absolute right-0 top-12 flex w-fit rounded-lg border border-white/20 bg-main p-4"
|
||||
>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!window) return;
|
||||
window.localStorage.removeItem(STORE_USER);
|
||||
window.localStorage.removeItem(STORE_TOKEN);
|
||||
window.location.replace(paths.home());
|
||||
}}
|
||||
type="button"
|
||||
className="w-full whitespace-nowrap rounded-md px-4 py-1.5 text-left text-white hover:bg-slate-200/20"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import Header from '../components/Header';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import Notifications from '../components/Notifications';
|
||||
import UserMenu from '../components/UserMenu';
|
||||
|
||||
interface DefaultLayoutProps {
|
||||
headerEntity: any;
|
||||
@@ -58,6 +60,11 @@ const AppLayout = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute right-0 top-0 mr-9 mt-7 flex items-center gap-x-2">
|
||||
<Notifications />
|
||||
<UserMenu />
|
||||
</div>
|
||||
<main>
|
||||
<div className="mx-auto rounded-tr-xl bg-main pr-6 pt-6">
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { INotification } from '../components/Header/Notifications';
|
||||
import { INotification } from '../components/Notifications';
|
||||
import { API_BASE } from '../utils/constants';
|
||||
import { baseHeaders, getAPIUrlString } from '../utils/request';
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@ import { useState } from 'react';
|
||||
import Organization from '../../../models/organization';
|
||||
import { Loader } from 'react-feather';
|
||||
import paths from '../../../utils/paths';
|
||||
import { CaretDown } from '@phosphor-icons/react';
|
||||
import showToast from '../../../utils/toast';
|
||||
|
||||
export default function OrgSettings({ organization }: { organization: any }) {
|
||||
const [hasOrgChanges, setHasOrgChanges] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [result, setResult] = useState<{
|
||||
show: boolean;
|
||||
success: boolean;
|
||||
error: null | string;
|
||||
}>({ show: false, success: false, error: null });
|
||||
|
||||
const handleOrgUpdate = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setResult({ show: false, success: false, error: null });
|
||||
|
||||
const form = new FormData(e.target);
|
||||
const newOrgName =
|
||||
(form.get('organization_name') as string) || organization?.name;
|
||||
@@ -23,11 +21,20 @@ export default function OrgSettings({ organization }: { organization: any }) {
|
||||
organization.slug,
|
||||
data
|
||||
);
|
||||
setResult({ show: true, success, error });
|
||||
|
||||
if (success) {
|
||||
showToast('Organization updated successfully, reloading...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
showToast(error, 'error');
|
||||
}
|
||||
success && setHasOrgChanges(false);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const handleDelete = async (e: any) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
!confirm(
|
||||
'Are you sure you want to do this. All associated information will be deleted.\n\nThis operation will ONLY remove information from Vector Admin and will not remove any data from your connected vector database.'
|
||||
@@ -46,72 +53,56 @@ export default function OrgSettings({ organization }: { organization: any }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="col-span-12 flex-1 rounded-sm bg-white pb-6 xl:col-span-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="mb-6 px-7.5 text-3xl font-semibold text-black dark:text-white">
|
||||
<div className="col-span-12 h-screen flex-1 rounded-sm bg-main pb-6 xl:col-span-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="-mt-10 flex items-center gap-x-4">
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="flex h-[34px] w-[34px] rotate-90 items-center justify-center rounded-full border border-transparent bg-zinc-900 text-white transition-all duration-300 hover:border-white/20 hover:bg-opacity-5 hover:text-white"
|
||||
>
|
||||
<CaretDown weight="bold" size={18} />
|
||||
</button>
|
||||
<div className="text-lg font-medium text-white">
|
||||
Organization Settings
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6">
|
||||
{result.show && (
|
||||
<>
|
||||
{result.success ? (
|
||||
<p className="my-2 w-full rounded-lg border-green-800 bg-green-50 px-4 py-2 text-lg text-green-800">
|
||||
Settings updated successfully.
|
||||
</p>
|
||||
) : (
|
||||
<p className="my-2 w-full rounded-lg border-red-800 bg-red-50 px-4 py-2 text-lg text-red-800">
|
||||
{result.error}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<form
|
||||
onChange={() => setHasOrgChanges(true)}
|
||||
onSubmit={handleOrgUpdate}
|
||||
className="border-b border-gray-200 pb-4"
|
||||
>
|
||||
<div className="my-4">
|
||||
<label className=" block flex items-center gap-x-1 font-medium text-black dark:text-white">
|
||||
Organization name
|
||||
</label>
|
||||
<p className="mb-2.5 text-sm text-slate-600">
|
||||
<div className="ml-13">
|
||||
<div className="mt-8.5 text-sm font-medium text-white">
|
||||
Organization Name
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-white text-opacity-60">
|
||||
This will only change the display name of the organization.
|
||||
</p>
|
||||
<div className="relative flex w-1/2 items-center gap-x-4">
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-x-4">
|
||||
<input
|
||||
type="text"
|
||||
name="organization_name"
|
||||
required={true}
|
||||
placeholder="My Organization"
|
||||
defaultValue={organization.name}
|
||||
required={true}
|
||||
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
|
||||
className="mt-2 inline-flex h-11 w-[210px] items-center justify-start gap-2.5 rounded-lg bg-white bg-opacity-10 p-2.5 text-sm font-medium leading-tight text-white text-opacity-60"
|
||||
/>
|
||||
<button
|
||||
hidden={!hasOrgChanges}
|
||||
type="submit"
|
||||
className="rounded-lg border border-primary px-8 py-3 text-lg text-blue-500 transition-all duration-300 hover:bg-primary hover:text-white"
|
||||
className="text-center text-sm font-medium text-sky-400"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="mt-5 w-fit rounded-lg p-1 text-sm leading-tight text-red-500 hover:bg-red-500 hover:text-white"
|
||||
>
|
||||
<Loader hidden={!deleting} size={16} className="animate-spin" />
|
||||
{deleting ? 'Removing Organization' : 'Delete Organization'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="my-2 flex w-full">
|
||||
<button
|
||||
disabled={deleting}
|
||||
onClick={handleDelete}
|
||||
className="flex items-center gap-x-1 rounded-lg bg-red-100 px-4 py-2 text-red-400 hover:bg-red-600 hover:text-white disabled:cursor-wait disabled:bg-red-600 disabled:text-white"
|
||||
>
|
||||
<Loader hidden={!deleting} size={16} className="animate-spin" />
|
||||
{deleting ? 'Removing Organization' : 'Delete Organization'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user