forked from phoenix/litellm-mirror
build(ui): add stubbed teams page
This commit is contained in:
parent
785a4a2ac2
commit
ad9169a4da
6 changed files with 458 additions and 13 deletions
|
@ -5,14 +5,16 @@ import Navbar from "../components/navbar";
|
||||||
import UserDashboard from "../components/user_dashboard";
|
import UserDashboard from "../components/user_dashboard";
|
||||||
import ModelDashboard from "@/components/model_dashboard";
|
import ModelDashboard from "@/components/model_dashboard";
|
||||||
import ViewUserDashboard from "@/components/view_users";
|
import ViewUserDashboard from "@/components/view_users";
|
||||||
|
import Teams from "@/components/teams";
|
||||||
import ChatUI from "@/components/chat_ui";
|
import ChatUI from "@/components/chat_ui";
|
||||||
import Sidebar from "../components/leftnav";
|
import Sidebar from "../components/leftnav";
|
||||||
import Usage from "../components/usage";
|
import Usage from "../components/usage";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
const CreateKeyPage = () => {
|
const CreateKeyPage = () => {
|
||||||
const [userRole, setUserRole] = useState('');
|
const [userRole, setUserRole] = useState("");
|
||||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
|
const [teams, setTeams] = useState<null | string[]>(null);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const userID = searchParams.get("userID");
|
const userID = searchParams.get("userID");
|
||||||
|
@ -74,14 +76,20 @@ const CreateKeyPage = () => {
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||||
<div className="flex flex-1 overflow-auto">
|
<div className="flex flex-1 overflow-auto">
|
||||||
<Sidebar setPage={setPage} userRole={userRole}/>
|
<Sidebar
|
||||||
|
setPage={setPage}
|
||||||
|
userRole={userRole}
|
||||||
|
defaultSelectedKey={null}
|
||||||
|
/>
|
||||||
{page == "api-keys" ? (
|
{page == "api-keys" ? (
|
||||||
<UserDashboard
|
<UserDashboard
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
|
teams={teams}
|
||||||
setUserRole={setUserRole}
|
setUserRole={setUserRole}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
setUserEmail={setUserEmail}
|
setUserEmail={setUserEmail}
|
||||||
|
setTeams={setTeams}
|
||||||
/>
|
/>
|
||||||
) : page == "models" ? (
|
) : page == "models" ? (
|
||||||
<ModelDashboard
|
<ModelDashboard
|
||||||
|
@ -97,16 +105,16 @@ const CreateKeyPage = () => {
|
||||||
token={token}
|
token={token}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
)
|
) : page == "users" ? (
|
||||||
: page == "users" ? (
|
|
||||||
<ViewUserDashboard
|
<ViewUserDashboard
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
token={token}
|
token={token}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
)
|
) : page == "teams" ? (
|
||||||
: (
|
<Teams teams={teams} searchParams={searchParams} />
|
||||||
|
) : (
|
||||||
<Usage
|
<Usage
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
|
|
149
ui/litellm-dashboard/src/app/team/page.tsx
Normal file
149
ui/litellm-dashboard/src/app/team/page.tsx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"use client";
|
||||||
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import Navbar from "../../components/navbar";
|
||||||
|
import Sidebar from "../../components/leftnav";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Card,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { CogIcon } from "@heroicons/react/outline";
|
||||||
|
|
||||||
|
const TeamSettingsPage = () => {
|
||||||
|
const [userRole, setUserRole] = useState("");
|
||||||
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
|
const [teams, setTeams] = useState<null | string[]>(null);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const userID = searchParams.get("userID");
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
const [page, setPage] = useState("team");
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||||
|
if (decoded) {
|
||||||
|
// cast decoded to dictionary
|
||||||
|
console.log("Decoded token:", decoded);
|
||||||
|
|
||||||
|
console.log("Decoded key:", decoded.key);
|
||||||
|
// set accessToken
|
||||||
|
setAccessToken(decoded.key);
|
||||||
|
|
||||||
|
// check if userRole is defined
|
||||||
|
if (decoded.user_role) {
|
||||||
|
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||||
|
console.log("Decoded user_role:", formattedUserRole);
|
||||||
|
setUserRole(formattedUserRole);
|
||||||
|
} else {
|
||||||
|
console.log("User role not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.user_email) {
|
||||||
|
setUserEmail(decoded.user_email);
|
||||||
|
} else {
|
||||||
|
console.log(`User Email is not set ${decoded}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
function formatUserRole(userRole: string) {
|
||||||
|
if (!userRole) {
|
||||||
|
return "Undefined Role";
|
||||||
|
}
|
||||||
|
console.log(`Received user role: ${userRole}`);
|
||||||
|
switch (userRole.toLowerCase()) {
|
||||||
|
case "app_owner":
|
||||||
|
return "App Owner";
|
||||||
|
case "demo_app_owner":
|
||||||
|
return "App Owner";
|
||||||
|
case "app_admin":
|
||||||
|
return "Admin";
|
||||||
|
case "app_user":
|
||||||
|
return "App User";
|
||||||
|
default:
|
||||||
|
return "Unknown Role";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||||
|
<div className="flex flex-1 overflow-auto">
|
||||||
|
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Settings</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Wilhelm Tell</TableCell>
|
||||||
|
<TableCell className="text-right">1</TableCell>
|
||||||
|
<TableCell>Uri, Schwyz, Unterwalden</TableCell>
|
||||||
|
<TableCell>National Hero</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>The Witcher</TableCell>
|
||||||
|
<TableCell className="text-right">129</TableCell>
|
||||||
|
<TableCell>Kaedwen</TableCell>
|
||||||
|
<TableCell>Legend</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Mizutsune</TableCell>
|
||||||
|
<TableCell className="text-right">82</TableCell>
|
||||||
|
<TableCell>Japan</TableCell>
|
||||||
|
<TableCell>N/A</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Link
|
||||||
|
href={`/team?userID=${searchParams.get(
|
||||||
|
"userID"
|
||||||
|
)}&token=${searchParams.get("token")}`}
|
||||||
|
>
|
||||||
|
<Button className="mx-auto">+ Create New Team</Button>
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default TeamSettingsPage;
|
|
@ -1,5 +1,6 @@
|
||||||
import { Layout, Menu } from "antd";
|
import { Layout, Menu } from "antd";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { List } from "postcss/lib/list";
|
||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
@ -7,15 +8,20 @@ const { Sider } = Layout;
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setPage: React.Dispatch<React.SetStateAction<string>>;
|
setPage: React.Dispatch<React.SetStateAction<string>>;
|
||||||
userRole: string;
|
userRole: string;
|
||||||
|
defaultSelectedKey: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({ setPage, userRole }) => {
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
setPage,
|
||||||
|
userRole,
|
||||||
|
defaultSelectedKey,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||||
<Sider width={120}>
|
<Sider width={120}>
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
defaultSelectedKeys={["1"]}
|
defaultSelectedKeys={defaultSelectedKey ? defaultSelectedKey : ["1"]}
|
||||||
style={{ height: "100%", borderRight: 0 }}
|
style={{ height: "100%", borderRight: 0 }}
|
||||||
>
|
>
|
||||||
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
||||||
|
|
|
@ -29,14 +29,19 @@ const Navbar: React.FC<NavbarProps> = ({ userID, userRole, userEmail }) => {
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
|
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4">
|
<nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4">
|
||||||
<div className="text-left mx-4 my-2 absolute top-0 left-0">
|
<div className="text-left my-2 absolute top-0 left-0">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="text-gray-800 text-2xl px-4 py-1 rounded text-center">
|
<button className="text-gray-800 text-2xl py-1 rounded text-center">
|
||||||
<img src={imageUrl} width={200} height={200} alt="LiteLLM Brand" className="mr-2" />
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
alt="LiteLLM Brand"
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
274
ui/litellm-dashboard/src/components/teams.tsx
Normal file
274
ui/litellm-dashboard/src/components/teams.tsx
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Typography } from "antd";
|
||||||
|
import {
|
||||||
|
Button as Button2,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select as Select2,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
} from "antd";
|
||||||
|
import { Select, SelectItem } from "@tremor/react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Card,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { CogIcon } from "@heroicons/react/outline";
|
||||||
|
interface TeamProps {
|
||||||
|
teams: string[] | null;
|
||||||
|
searchParams: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Team: React.FC<TeamProps> = ({ teams, searchParams }) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [memberForm] = Form.useForm();
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
||||||
|
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||||
|
const handleOk = () => {
|
||||||
|
setIsTeamModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberOk = () => {
|
||||||
|
setIsAddMemberModalVisible(false);
|
||||||
|
memberForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsTeamModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberCancel = () => {
|
||||||
|
setIsAddMemberModalVisible(false);
|
||||||
|
memberForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (formValues: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
console.log("reaches here");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating the key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
console.log("reaches here");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating the key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log(`received teams ${teams}`);
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Grid numItems={1} className="gap-2 p-2 h-[75vh] w-full">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Title level={4}>All Teams</Title>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Wilhelm Tell</TableCell>
|
||||||
|
<TableCell className="text-right">1</TableCell>
|
||||||
|
<TableCell>Uri, Schwyz, Unterwalden</TableCell>
|
||||||
|
<TableCell>National Hero</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>The Witcher</TableCell>
|
||||||
|
<TableCell className="text-right">129</TableCell>
|
||||||
|
<TableCell>Kaedwen</TableCell>
|
||||||
|
<TableCell>Legend</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Mizutsune</TableCell>
|
||||||
|
<TableCell className="text-right">82</TableCell>
|
||||||
|
<TableCell>Japan</TableCell>
|
||||||
|
<TableCell>N/A</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Button
|
||||||
|
className="mx-auto"
|
||||||
|
onClick={() => setIsTeamModalVisible(true)}
|
||||||
|
>
|
||||||
|
+ Create New Team
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="Create Team"
|
||||||
|
visible={isTeamModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleCreate}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Form.Item label="Team Name" name="team_alias">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Models" name="models">
|
||||||
|
<Select2
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="Select models"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{/* {userModels.map((model) => (
|
||||||
|
<Option key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</Option>
|
||||||
|
))} */}
|
||||||
|
</Select2>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||||
|
<InputNumber step={0.01} precision={2} width={200} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Tokens per minute Limit (TPM)"
|
||||||
|
name="tpm_limit"
|
||||||
|
>
|
||||||
|
<InputNumber step={1} width={400} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Requests per minute Limit (RPM)"
|
||||||
|
name="rpm_limit"
|
||||||
|
>
|
||||||
|
<InputNumber step={1} width={400} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
|
<Button2 htmlType="submit">Create Team</Button2>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Title level={4}>Team Members</Title>
|
||||||
|
<Paragraph>
|
||||||
|
If you belong to multiple teams, this setting controls which teams'
|
||||||
|
members you see.
|
||||||
|
</Paragraph>
|
||||||
|
{teams && teams.length > 0 ? (
|
||||||
|
<Select
|
||||||
|
id="distance"
|
||||||
|
value={value}
|
||||||
|
onValueChange={setValue}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
{teams.map((model) => (
|
||||||
|
<SelectItem value="model">{model}</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Paragraph>
|
||||||
|
No team created. <b>Defaulting to personal account.</b>
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Member Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Role</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Action</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Wilhelm Tell</TableCell>
|
||||||
|
<TableCell>Uri, Schwyz, Unterwalden</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>The Witcher</TableCell>
|
||||||
|
<TableCell>Kaedwen</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Mizutsune</TableCell>
|
||||||
|
<TableCell>Japan</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Button
|
||||||
|
className="mx-auto mb-5"
|
||||||
|
onClick={() => setIsAddMemberModalVisible(true)}
|
||||||
|
>
|
||||||
|
+ Add member
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="Add member"
|
||||||
|
visible={isAddMemberModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onOk={handleMemberOk}
|
||||||
|
onCancel={handleMemberCancel}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleMemberCreate}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Form.Item label="Email" name="user_email">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
|
<Button2 htmlType="submit">Add member</Button2>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Team;
|
|
@ -25,19 +25,22 @@ interface UserDashboardProps {
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
|
teams: string[] | null;
|
||||||
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
setTeams: React.Dispatch<React.SetStateAction<string[] | null>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserDashboard: React.FC<UserDashboardProps> = ({
|
const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
userID,
|
userID,
|
||||||
userRole,
|
userRole,
|
||||||
|
teams,
|
||||||
setUserRole,
|
setUserRole,
|
||||||
userEmail,
|
userEmail,
|
||||||
setUserEmail,
|
setUserEmail,
|
||||||
|
setTeams,
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
||||||
const [teams, setTeams] = useState<null | string[]>(null); // Keep the initialization of state here
|
|
||||||
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue