forked from phoenix/litellm-mirror
Merge pull request #3868 from BerriAI/litellm_show_updated_created_models
[Feat] Show Created at, Created by on `Models` Page
This commit is contained in:
commit
d71bb96047
4 changed files with 149 additions and 59 deletions
|
@ -21,6 +21,7 @@ Features:
|
||||||
- ✅ Don't log/store specific requests to Langfuse, Sentry, etc. (eg confidential LLM requests)
|
- ✅ Don't log/store specific requests to Langfuse, Sentry, etc. (eg confidential LLM requests)
|
||||||
- ✅ Tracking Spend for Custom Tags
|
- ✅ Tracking Spend for Custom Tags
|
||||||
- ✅ Custom Branding + Routes on Swagger Docs
|
- ✅ Custom Branding + Routes on Swagger Docs
|
||||||
|
- ✅ Audit Logs for `Created At, Created By` when Models Added
|
||||||
|
|
||||||
|
|
||||||
## Content Moderation
|
## Content Moderation
|
||||||
|
|
|
@ -2790,6 +2790,13 @@ class ProxyConfig:
|
||||||
model.model_info["id"] = _id
|
model.model_info["id"] = _id
|
||||||
model.model_info["db_model"] = True
|
model.model_info["db_model"] = True
|
||||||
|
|
||||||
|
if premium_user is True:
|
||||||
|
# seeing "created_at", "updated_at", "created_by", "updated_by" is a LiteLLM Enterprise Feature
|
||||||
|
model.model_info["created_at"] = getattr(model, "created_at", None)
|
||||||
|
model.model_info["updated_at"] = getattr(model, "updated_at", None)
|
||||||
|
model.model_info["created_by"] = getattr(model, "created_by", None)
|
||||||
|
model.model_info["updated_by"] = getattr(model, "updated_by", None)
|
||||||
|
|
||||||
if model.model_info is not None and isinstance(model.model_info, dict):
|
if model.model_info is not None and isinstance(model.model_info, dict):
|
||||||
if "id" not in model.model_info:
|
if "id" not in model.model_info:
|
||||||
model.model_info["id"] = model.model_id
|
model.model_info["id"] = model.model_id
|
||||||
|
@ -3075,10 +3082,9 @@ class ProxyConfig:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if master_key is None or not isinstance(master_key, str):
|
if master_key is None or not isinstance(master_key, str):
|
||||||
raise Exception(
|
raise ValueError(
|
||||||
f"Master key is not initialized or formatted. master_key={master_key}"
|
f"Master key is not initialized or formatted. master_key={master_key}"
|
||||||
)
|
)
|
||||||
verbose_proxy_logger.debug(f"llm_router: {llm_router}")
|
|
||||||
new_models = await prisma_client.db.litellm_proxymodeltable.find_many()
|
new_models = await prisma_client.db.litellm_proxymodeltable.find_many()
|
||||||
# update llm router
|
# update llm router
|
||||||
await self._update_llm_router(
|
await self._update_llm_router(
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
"""
|
||||||
|
litellm.Router Types - includes RouterConfig, UpdateRouterConfig, ModelInfo etc
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import List, Optional, Union, Dict, Tuple, Literal, TypedDict
|
from typing import List, Optional, Union, Dict, Tuple, Literal, TypedDict
|
||||||
|
import uuid
|
||||||
|
import enum
|
||||||
import httpx
|
import httpx
|
||||||
from pydantic import BaseModel, validator, Field
|
from pydantic import BaseModel, Field
|
||||||
|
import datetime
|
||||||
from .completion import CompletionRequest
|
from .completion import CompletionRequest
|
||||||
from .embedding import EmbeddingRequest
|
from .embedding import EmbeddingRequest
|
||||||
import uuid, enum
|
|
||||||
|
|
||||||
|
|
||||||
class ModelConfig(BaseModel):
|
class ModelConfig(BaseModel):
|
||||||
|
@ -76,6 +82,12 @@ class ModelInfo(BaseModel):
|
||||||
db_model: bool = (
|
db_model: bool = (
|
||||||
False # used for proxy - to separate models which are stored in the db vs. config.
|
False # used for proxy - to separate models which are stored in the db vs. config.
|
||||||
)
|
)
|
||||||
|
updated_at: Optional[datetime.datetime] = None
|
||||||
|
updated_by: Optional[str] = None
|
||||||
|
|
||||||
|
created_at: Optional[datetime.datetime] = None
|
||||||
|
created_by: Optional[str] = None
|
||||||
|
|
||||||
base_model: Optional[str] = (
|
base_model: Optional[str] = (
|
||||||
None # specify if the base model is azure/gpt-3.5-turbo etc for accurate cost tracking
|
None # specify if the base model is azure/gpt-3.5-turbo etc for accurate cost tracking
|
||||||
)
|
)
|
||||||
|
|
|
@ -86,6 +86,8 @@ import type { UploadProps } from "antd";
|
||||||
import { Upload } from "antd";
|
import { Upload } from "antd";
|
||||||
import TimeToFirstToken from "./model_metrics/time_to_first_token";
|
import TimeToFirstToken from "./model_metrics/time_to_first_token";
|
||||||
import DynamicFields from "./model_add/dynamic_form";
|
import DynamicFields from "./model_add/dynamic_form";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
|
||||||
interface ModelDashboardProps {
|
interface ModelDashboardProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
@ -269,6 +271,8 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
const [selectedProvider, setSelectedProvider] = useState<String>("OpenAI");
|
const [selectedProvider, setSelectedProvider] = useState<String>("OpenAI");
|
||||||
const [healthCheckResponse, setHealthCheckResponse] = useState<string>("");
|
const [healthCheckResponse, setHealthCheckResponse] = useState<string>("");
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||||
|
const [infoModalVisible, setInfoModalVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
const [selectedModel, setSelectedModel] = useState<any>(null);
|
const [selectedModel, setSelectedModel] = useState<any>(null);
|
||||||
const [availableModelGroups, setAvailableModelGroups] = useState<
|
const [availableModelGroups, setAvailableModelGroups] = useState<
|
||||||
Array<string>
|
Array<string>
|
||||||
|
@ -297,6 +301,15 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
useState<RetryPolicyObject | null>(null);
|
useState<RetryPolicyObject | null>(null);
|
||||||
const [defaultRetry, setDefaultRetry] = useState<number>(0);
|
const [defaultRetry, setDefaultRetry] = useState<number>(0);
|
||||||
|
|
||||||
|
function formatCreatedAt(createdAt: string | null) {
|
||||||
|
if (createdAt) {
|
||||||
|
const date = new Date(createdAt);
|
||||||
|
const options = { month: 'long', day: 'numeric', year: 'numeric' };
|
||||||
|
return date.toLocaleDateString('en-US');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const EditModelModal: React.FC<EditModelModalProps> = ({
|
const EditModelModal: React.FC<EditModelModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
@ -423,11 +436,21 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
setEditModalVisible(true);
|
setEditModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInfoClick = (model: any) => {
|
||||||
|
setSelectedModel(model);
|
||||||
|
setInfoModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleEditCancel = () => {
|
const handleEditCancel = () => {
|
||||||
setEditModalVisible(false);
|
setEditModalVisible(false);
|
||||||
setSelectedModel(null);
|
setSelectedModel(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInfoCancel = () => {
|
||||||
|
setInfoModalVisible(false);
|
||||||
|
setSelectedModel(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleEditSubmit = async (formValues: Record<string, any>) => {
|
const handleEditSubmit = async (formValues: Record<string, any>) => {
|
||||||
// Call API to update team with teamId and values
|
// Call API to update team with teamId and values
|
||||||
|
|
||||||
|
@ -1039,7 +1062,6 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
<Table
|
<Table
|
||||||
className="mt-5"
|
|
||||||
style={{ maxWidth: "1500px", width: "100%" }}
|
style={{ maxWidth: "1500px", width: "100%" }}
|
||||||
>
|
>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
@ -1049,6 +1071,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
maxWidth: "150px",
|
maxWidth: "150px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Public Model Name
|
Public Model Name
|
||||||
|
@ -1058,6 +1081,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
maxWidth: "100px",
|
maxWidth: "100px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Provider
|
Provider
|
||||||
|
@ -1068,25 +1092,18 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
maxWidth: "150px",
|
maxWidth: "150px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
API Base
|
API Base
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
)}
|
)}
|
||||||
<TableHeaderCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "200px",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Extra litellm Params
|
|
||||||
</TableHeaderCell>
|
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "85px",
|
maxWidth: "85px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Input Price{" "}
|
Input Price{" "}
|
||||||
|
@ -1099,6 +1116,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
maxWidth: "85px",
|
maxWidth: "85px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Output Price{" "}
|
Output Price{" "}
|
||||||
|
@ -1106,24 +1124,45 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
/1M Tokens ($)
|
/1M Tokens ($)
|
||||||
</p>
|
</p>
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
|
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "120px",
|
maxWidth: "100px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Max Tokens
|
{
|
||||||
|
premiumUser ? "Created At" : <a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank" style={{color: "#72bcd4" }}> ✨ Created At</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell
|
||||||
|
style={{
|
||||||
|
maxWidth: "100px",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
premiumUser ? "Created By" : <a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank" style={{color: "#72bcd4" }}> ✨ Created By</a>
|
||||||
|
}
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "50px",
|
maxWidth: "50px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "11px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Status
|
Status
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
|
||||||
|
</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -1137,15 +1176,17 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
selectedModelGroup === ""
|
selectedModelGroup === ""
|
||||||
)
|
)
|
||||||
.map((model: any, index: number) => (
|
.map((model: any, index: number) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index} style={{ maxHeight: "1px", minHeight: "1px" }}>
|
||||||
<TableCell
|
<TableCell
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "150px",
|
maxWidth: "100px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>{model.model_name}</Text>
|
<p style={{ fontSize: "10px" }}>
|
||||||
|
{model.model_name || "-"}
|
||||||
|
</p>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
style={{
|
style={{
|
||||||
|
@ -1154,41 +1195,34 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{model.provider}
|
<p style={{ fontSize: "10px" }}>
|
||||||
|
{model.provider || "-"}
|
||||||
|
</p>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{userRole === "Admin" && (
|
{userRole === "Admin" && (
|
||||||
<TableCell
|
<TableCell
|
||||||
|
style={{
|
||||||
|
maxWidth: "150px",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title={model && model.api_base}>
|
||||||
|
<pre
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "150px",
|
maxWidth: "150px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontSize: "10px",
|
||||||
}}
|
}}
|
||||||
|
title={model && model.api_base ? model.api_base : ""}
|
||||||
>
|
>
|
||||||
{model.api_base}
|
{model && model.api_base ? model.api_base.slice(0, 20) : "-"}
|
||||||
</TableCell>
|
</pre>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "200px",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Accordion>
|
|
||||||
<AccordionHeader>
|
|
||||||
<Text>Litellm params</Text>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(
|
|
||||||
model.cleanedLitellmParams,
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
<TableCell
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "80px",
|
maxWidth: "80px",
|
||||||
|
@ -1196,9 +1230,11 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<pre style={{ fontSize: "10px" }}>
|
||||||
{model.input_cost ||
|
{model.input_cost ||
|
||||||
model.litellm_params.input_cost_per_token ||
|
model.litellm_params.input_cost_per_token ||
|
||||||
null}
|
"-"}
|
||||||
|
</pre>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
style={{
|
style={{
|
||||||
|
@ -1207,20 +1243,25 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<pre style={{ fontSize: "10px" }}>
|
||||||
{model.output_cost ||
|
{model.output_cost ||
|
||||||
model.litellm_params.output_cost_per_token ||
|
model.litellm_params.output_cost_per_token ||
|
||||||
null}
|
"-"}
|
||||||
|
</pre>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell>
|
||||||
style={{
|
<p style={{ fontSize: "10px" }}>
|
||||||
maxWidth: "120px",
|
{
|
||||||
whiteSpace: "normal",
|
premiumUser ? formatCreatedAt(model.model_info.created_at) || "-" : "-"
|
||||||
wordBreak: "break-word",
|
}
|
||||||
}}
|
</p>
|
||||||
>
|
|
||||||
<p style={{ fontSize: "10px" }}>
|
</TableCell>
|
||||||
Max Tokens: {model.max_tokens} <br></br>
|
<TableCell>
|
||||||
Max Input Tokens: {model.max_input_tokens}
|
<p style={{ fontSize: "10px" }}>
|
||||||
|
{
|
||||||
|
premiumUser ? model.model_info.created_by || "-" : "-"
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
|
@ -1236,7 +1277,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
size="xs"
|
size="xs"
|
||||||
className="text-white"
|
className="text-white"
|
||||||
>
|
>
|
||||||
<p style={{ fontSize: "10px" }}>DB Model</p>
|
<p style={{ fontSize: "8px" }}>DB Model</p>
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge
|
<Badge
|
||||||
|
@ -1244,26 +1285,42 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
size="xs"
|
size="xs"
|
||||||
className="text-black"
|
className="text-black"
|
||||||
>
|
>
|
||||||
<p style={{ fontSize: "10px" }}>Config Model</p>
|
<p style={{ fontSize: "8px" }}>Config Model</p>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100px",
|
maxWidth: "150px",
|
||||||
whiteSpace: "normal",
|
whiteSpace: "normal",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Grid numItems={3}>
|
||||||
|
<Col>
|
||||||
|
<Icon
|
||||||
|
icon={InformationCircleIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleInfoClick(model)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
<Icon
|
<Icon
|
||||||
icon={PencilAltIcon}
|
icon={PencilAltIcon}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEditClick(model)}
|
onClick={() => handleEditClick(model)}
|
||||||
/>
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col>
|
||||||
<DeleteModelButton
|
<DeleteModelButton
|
||||||
modelID={model.model_info.id}
|
modelID={model.model_info.id}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
@ -1277,6 +1334,20 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
model={selectedModel}
|
model={selectedModel}
|
||||||
onSubmit={handleEditSubmit}
|
onSubmit={handleEditSubmit}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
title={selectedModel && selectedModel.model_name}
|
||||||
|
visible={infoModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onCancel={handleInfoCancel}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Title>Model Info</Title>
|
||||||
|
<SyntaxHighlighter language="json" >
|
||||||
|
{selectedModel && JSON.stringify(selectedModel, null, 2)}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
|
||||||
|
</Modal>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel className="h-full">
|
<TabPanel className="h-full">
|
||||||
<Title2 level={2}>Add new model</Title2>
|
<Title2 level={2}>Add new model</Title2>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue