Merge branch 'develop' into feature/node_run
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-04 17:20:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-14 18:24:29
|
||||
* @Last Modified time: 2026-04-16 11:46:39
|
||||
*/
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
@@ -35,7 +35,7 @@ interface CodeMirrorEditorProps {
|
||||
height?: string;
|
||||
size?: 'default' | 'small';
|
||||
placeholder?: string;
|
||||
variant?: 'outlined' | 'borderless';
|
||||
variant?: 'outlined' | 'borderless' | 'filled';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +156,7 @@ const CodeMirrorEditor = ({
|
||||
<div
|
||||
ref={editorRef}
|
||||
style={{ minHeight, fontSize, lineHeight }}
|
||||
className={variant === 'borderless' ? '' : 'rb-border rb:rounded-lg'}
|
||||
className={variant === 'outlined' ? 'rb-border rb:rounded-lg' : variant === 'filled' ? 'cm-editor-filled rb:rounded-lg' : ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
.breadcrumbTitle {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.header :global(.ant-breadcrumb) {
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:07:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 12:18:58
|
||||
* @Last Modified time: 2026-04-16 10:31:21
|
||||
*/
|
||||
/**
|
||||
* AppHeader Component
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { type FC, useRef, useState } from 'react';
|
||||
import { Layout, Dropdown, Breadcrumb, Flex } from 'antd';
|
||||
import { Layout, Dropdown, Breadcrumb, Flex, Tooltip } from 'antd';
|
||||
import type { MenuProps, BreadcrumbProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -31,7 +31,7 @@ const { Header } = Layout;
|
||||
/**
|
||||
* @param source - Breadcrumb source type ('space' or 'manage'), defaults to 'manage'
|
||||
*/
|
||||
const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
const AppHeader: FC<{ source?: 'space' | 'manage'; }> = ({ source = 'manage' }) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const settingModalRef = useRef<SettingModalRef>(null)
|
||||
@@ -39,7 +39,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
|
||||
const { user, logout } = useUser();
|
||||
const { allBreadcrumbs } = useMenu();
|
||||
|
||||
|
||||
/**
|
||||
* Dynamically select breadcrumb source based on current route
|
||||
* - Knowledge base list: uses 'space' breadcrumb
|
||||
@@ -48,24 +48,24 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
*/
|
||||
const getBreadcrumbSource = () => {
|
||||
const pathname = location.pathname;
|
||||
|
||||
|
||||
// Knowledge base list page uses default space breadcrumb
|
||||
if (pathname === '/knowledge-base') {
|
||||
return 'space';
|
||||
}
|
||||
|
||||
|
||||
// Knowledge base detail pages use independent breadcrumb
|
||||
if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') {
|
||||
return 'space-detail';
|
||||
}
|
||||
|
||||
|
||||
// Other pages use the passed source
|
||||
return source;
|
||||
};
|
||||
|
||||
|
||||
const breadcrumbSource = getBreadcrumbSource();
|
||||
const breadcrumbs = allBreadcrumbs[breadcrumbSource] || [];
|
||||
|
||||
|
||||
|
||||
/** Handle user logout */
|
||||
const handleLogout = () => {
|
||||
@@ -76,9 +76,11 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
const userMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
icon: <Flex align="center" justify="center" className="rb:size-10 rb:rounded-xl rb:bg-[#155EEF] rb:text-white">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(0, 2) : user.username?.[0]}
|
||||
</Flex>,
|
||||
icon: user.username
|
||||
? <Flex align="center" justify="center" className="rb:size-10 rb:rounded-xl rb:bg-[#155EEF] rb:text-white">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(-2) : user.username[0]}
|
||||
</Flex>
|
||||
: null,
|
||||
label: (<>
|
||||
<div className="rb:text-[#212332] rb:leading-5">{user.username}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#7B8085] rb:leading-4.5 rb:mt-0.5 rb:mr-2">{user.email}</div>
|
||||
@@ -127,7 +129,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Format breadcrumb items with proper titles, paths, and click handlers
|
||||
* - Translates i18n keys to display text
|
||||
@@ -135,32 +137,34 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
* - Disables navigation for the last breadcrumb item
|
||||
*/
|
||||
const formatBreadcrumbNames = () => {
|
||||
return breadcrumbs.filter(item => item.type !== 'group').map((menu, index) => {
|
||||
const filtered = breadcrumbs.filter(item => item.type !== 'group');
|
||||
return filtered.map((menu, index) => {
|
||||
const label = menu.i18nKey ? t(menu.i18nKey) : menu.label;
|
||||
const isLast = index === filtered.length - 1;
|
||||
const item: any = {
|
||||
title: menu.i18nKey ? t(menu.i18nKey) : menu.label,
|
||||
title: (
|
||||
<Tooltip title={label} placement="bottom">
|
||||
<span className={styles.breadcrumbTitle}>{label}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
};
|
||||
|
||||
// If it's the last item, don't set path
|
||||
if (index === breadcrumbs.length - 1) {
|
||||
return item;
|
||||
|
||||
if (!isLast) {
|
||||
if ((menu as any).onClick) {
|
||||
item.onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
(menu as any).onClick(e);
|
||||
};
|
||||
item.href = '#';
|
||||
} else if (menu.path && menu.path !== '#') {
|
||||
item.path = menu.path;
|
||||
}
|
||||
}
|
||||
|
||||
// If has custom onClick, use onClick and set href to '#' to show pointer cursor
|
||||
if ((menu as any).onClick) {
|
||||
item.onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
(menu as any).onClick(e);
|
||||
};
|
||||
item.href = '#';
|
||||
} else if (menu.path && menu.path !== '#') {
|
||||
// Only set path when path is not '#'
|
||||
item.path = menu.path;
|
||||
}
|
||||
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
@@ -179,9 +183,9 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
overlayClassName={styles.userDropdown}
|
||||
>
|
||||
<Flex align="center" className="rb:cursor-pointer rb:font-medium">
|
||||
<Flex align="center" justify="center" className="rb:size-8 rb:rounded-xl rb:bg-[#155EEF] rb:text-white rb:mr-2!">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(user.username.length, -2) : user.username[0]}
|
||||
</Flex>
|
||||
{user.username && <Flex align="center" justify="center" className="rb:size-8 rb:rounded-xl rb:bg-[#155EEF] rb:text-white rb:mr-2!">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(-2) : user.username[0]}
|
||||
</Flex>}
|
||||
<span className="rb:text-[#212332] rb:text-[12px] rb:leading-4 rb:mr-1">{user.username}</span>
|
||||
<div className={clsx("rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')]", {
|
||||
'rb:rotate-180': !open,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-07 16:49:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-25 11:21:59
|
||||
* @Last Modified time: 2026-04-17 10:11:54
|
||||
*/
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { Select, Flex, Space } from 'antd';
|
||||
import type { SelectProps } from 'antd/es/select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -22,16 +22,10 @@ interface ModelSelectProps extends SelectProps {
|
||||
fontClassName?: string;
|
||||
isAutoFetch?: boolean;
|
||||
initialData?: Model[];
|
||||
updateOptions?: (options: Model[]) => void;
|
||||
}
|
||||
|
||||
const ModelSelect: FC<ModelSelectProps> = ({
|
||||
params,
|
||||
placeholder,
|
||||
fontClassName,
|
||||
isAutoFetch = true,
|
||||
initialData = [],
|
||||
...props
|
||||
}) => {
|
||||
const ModelSelect: FC<ModelSelectProps> = ({ params, placeholder, fontClassName, isAutoFetch = true, initialData = [], updateOptions, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<Model[]>([]);
|
||||
|
||||
@@ -60,6 +54,10 @@ const ModelSelect: FC<ModelSelectProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (updateOptions) updateOptions([...options, ...initialData]);
|
||||
}, [options, initialData])
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder={placeholder ?? t('common.pleaseSelect')}
|
||||
|
||||
@@ -126,7 +126,6 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
||||
// 内容不足以填满容器时,主动继续加载
|
||||
setTimeout(() => {
|
||||
const el = scrollRef.current;
|
||||
console.log(el, el?.scrollHeight, el?.clientHeight, hasMoreRef.current)
|
||||
if (el && hasMoreRef.current && el.scrollHeight <= el.clientHeight) {
|
||||
loadMoreData();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.page-tabs:global(.ant-segmented) {
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.page-tabs:global(.ant-segmented .ant-segmented-item-label) {
|
||||
line-height: 24px;
|
||||
|
||||
@@ -44,6 +44,7 @@ const RbSlider: FC<RbSliderProps> = ({
|
||||
className = '',
|
||||
prefix,
|
||||
inputClassName,
|
||||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const [curValue, setCurValue] = useState<SliderSingleProps['value']>(0)
|
||||
@@ -83,6 +84,7 @@ const RbSlider: FC<RbSliderProps> = ({
|
||||
max={max}
|
||||
step={step}
|
||||
value={curValue}
|
||||
disabled={disabled}
|
||||
onChange={handleSliderChange}
|
||||
classNames={size === 'small' ? {
|
||||
rail: 'rb:w-[calc(100%-6px)]!'
|
||||
@@ -96,6 +98,7 @@ const RbSlider: FC<RbSliderProps> = ({
|
||||
max={max}
|
||||
step={step as number}
|
||||
value={curValue}
|
||||
disabled={disabled}
|
||||
onChange={handleInputChange}
|
||||
prefix={prefix}
|
||||
className={`${inputClassName || '' } rb:w-20!`}
|
||||
|
||||
119
web/src/components/SiderMenu/SubscriptionDetailModal.tsx
Normal file
119
web/src/components/SiderMenu/SubscriptionDetailModal.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 12:28:23
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-16 17:34:02
|
||||
*/
|
||||
|
||||
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Flex, Tooltip, Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import RbModal from '@/components/RbModal';
|
||||
import type { Subscription } from './index'
|
||||
import { billingUnits } from '@/views/Package/constant'
|
||||
import { useI18n } from '@/store/locale'
|
||||
import { UnitWrapper } from '@/views/Package'
|
||||
|
||||
export interface SubscriptionDetailModalRef {
|
||||
handleOpen: (subscription: Subscription | null) => void;
|
||||
}
|
||||
|
||||
const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { language } = useI18n()
|
||||
const [detail, setDetail] = useState<Subscription | null>(null);
|
||||
|
||||
const handleOpen = (subscription: Subscription | null) => {
|
||||
setOpen(true)
|
||||
setDetail(subscription);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
const getKeyWithLanguage = (key: string) => {
|
||||
return (language === 'en' ? `${key}_en` : key) as keyof Subscription['package_plan']
|
||||
}
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={[t('package.packageDetail'), detail?.package_plan?.[getKeyWithLanguage('name')]].filter(item => item).join(' - ')}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
>
|
||||
{/* Header */}
|
||||
<h3 className="rb:text-[18px] rb:font-bold rb:text-[MiSans-Bold]" style={{ color: detail?.package_plan?.theme_color }}>
|
||||
{String(detail?.package_plan?.[getKeyWithLanguage('name')] ?? '')}
|
||||
</h3>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="rb:text-[#5B6167] rb:mb-3">
|
||||
{String(detail?.package_plan?.[getKeyWithLanguage('core_value')] ?? '')}
|
||||
</p>
|
||||
|
||||
{/* Price */}
|
||||
<div className="rb:h-10">
|
||||
{detail?.package_plan?.billing_cycle !== 'permanent_free' && <>
|
||||
<span className="rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:mr-1">¥</span>
|
||||
<span className="rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10">{detail?.package_plan?.price}</span>
|
||||
</>}
|
||||
{detail?.package_plan?.billing_cycle && (
|
||||
<span className={clsx({
|
||||
'rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10': detail?.package_plan?.billing_cycle === 'permanent_free',
|
||||
'rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:ml-1': detail?.package_plan?.billing_cycle !== 'permanent_free'
|
||||
})}>
|
||||
{detail?.package_plan?.billing_cycle !== 'permanent_free' && ' /'}
|
||||
{t(`package.${detail?.package_plan?.billing_cycle}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="rb:my-4" />
|
||||
|
||||
{/* Features */}
|
||||
<Flex gap={12} vertical className="rb:space-y-3 rb:mb-4 rb:h-[calc(100vh-341px)]! rb:overflow-y-auto">
|
||||
{billingUnits.map(({ key, unit, icon }) => {
|
||||
const value = detail?.quota[key as keyof Subscription['quota']];
|
||||
if (value === undefined || value === null) return null;
|
||||
return (
|
||||
<UnitWrapper
|
||||
key={key}
|
||||
titleKey={key}
|
||||
value={value}
|
||||
unit={unit}
|
||||
icon={icon}
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{detail?.package_plan?.tech_support && (
|
||||
<UnitWrapper
|
||||
titleKey="tech_support"
|
||||
value={String(detail?.package_plan?.[getKeyWithLanguage('tech_support')] ?? '')}
|
||||
icon="technical_support"
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)}
|
||||
{detail?.package_plan?.sla_compliance && (
|
||||
<UnitWrapper
|
||||
titleKey="sla"
|
||||
value={String(detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||
icon="sla"
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SubscriptionDetailModal;
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:25:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-27 19:11:43
|
||||
* @Last Modified time: 2026-04-16 17:35:38
|
||||
*/
|
||||
/**
|
||||
* SiderMenu Component
|
||||
@@ -18,7 +18,7 @@
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { useState, useEffect, type FC } from 'react';
|
||||
import { useState, useEffect, useRef, type FC } from 'react';
|
||||
import { Menu as AntMenu, Layout, Flex } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -30,6 +30,9 @@ import { useMenu, type MenuItem } from '@/store/menu';
|
||||
import styles from './index.module.css'
|
||||
import logo from '@/assets/images/logo.png'
|
||||
import { useUser } from '@/store/user';
|
||||
import { getTenantSubscription } from '@/api/user';
|
||||
import { useI18n } from '@/store/locale'
|
||||
import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal'
|
||||
|
||||
// Import SVG files
|
||||
// space
|
||||
@@ -70,7 +73,51 @@ import pricingActiveIcon from '@/assets/images/menuNew/pricing_active.svg'
|
||||
import skillsIcon from '@/assets/images/menuNew/skills.svg'
|
||||
import skillsActiveIcon from '@/assets/images/menuNew/skills_active.svg'
|
||||
|
||||
export interface PackagePlan {
|
||||
id: string
|
||||
name: string
|
||||
name_en?: string
|
||||
version: string
|
||||
category: string
|
||||
tier_level: number
|
||||
price: number
|
||||
billing_cycle: string
|
||||
core_value?: string
|
||||
core_value_en?: string
|
||||
tech_support?: string
|
||||
tech_support_en?: string
|
||||
sla_compliance?: string
|
||||
sla_compliance_en?: string
|
||||
page_customization?: string
|
||||
page_customization_en?: string
|
||||
theme_color?: string
|
||||
}
|
||||
|
||||
export interface SubscriptionQuota {
|
||||
app_quota: number
|
||||
model_quota: number
|
||||
skill_quota: number
|
||||
end_user_quota: number
|
||||
workspace_quota: number
|
||||
api_ops_rate_limit: number
|
||||
memory_engine_quota: number
|
||||
ontology_project_quota: number
|
||||
knowledge_capacity_quota: number
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
subscription_id: string | null
|
||||
tenant_id: string
|
||||
package_plan_id: string
|
||||
package_version: string
|
||||
package_plan: PackagePlan
|
||||
started_at: number | null
|
||||
expired_at: number | null
|
||||
status: string
|
||||
quota: SubscriptionQuota
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
/** Icon path mapping table for menu items (normal and active states) */
|
||||
const iconPathMap: Record<string, string> = {
|
||||
'dashboard': dashboardIcon,
|
||||
@@ -121,10 +168,12 @@ const Menu: FC<{
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { language } = useI18n()
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const { allMenus, collapsed, loadMenus, toggleSider } = useMenu()
|
||||
const [menus, setMenus] = useState<MenuItem[]>([])
|
||||
const { user, storageType } = useUser()
|
||||
const subscriptionDetailRef = useRef<SubscriptionDetailModalRef>(null)
|
||||
|
||||
/** Filter menus based on user role and source */
|
||||
useEffect(() => {
|
||||
@@ -279,6 +328,25 @@ const Menu: FC<{
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null)
|
||||
useEffect(() => {
|
||||
if (source === 'manage') {
|
||||
getTenantSubscription()
|
||||
.then(res => {
|
||||
setSubscription(res as Subscription)
|
||||
})
|
||||
} else {
|
||||
setSubscription(null)
|
||||
}
|
||||
}, [source])
|
||||
|
||||
const getKeyWithLanguage = (key: string) => {
|
||||
return (language === 'en' ? `${key}_en` : key) as keyof Subscription['package_plan']
|
||||
}
|
||||
const handleViewDetail = () => {
|
||||
subscriptionDetailRef.current?.handleOpen(subscription)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sider
|
||||
width={240}
|
||||
@@ -325,7 +393,8 @@ const Menu: FC<{
|
||||
inlineIndent={10}
|
||||
className={clsx("rb:overflow-y-auto", {
|
||||
'rb:max-h-[calc(100vh-136px)]': user?.is_superuser && source === 'space',
|
||||
'rb:max-h-[calc(100vh-76px)]': !(user?.is_superuser && source === 'space')
|
||||
'rb:max-h-[calc(100vh-76px)]': !(user?.is_superuser && source === 'space') && !(source === 'manage' && subscription && !collapsed),
|
||||
'rb:max-h-[calc(100vh-228px)]': source === 'manage' && subscription && !collapsed,
|
||||
})}
|
||||
/>
|
||||
{/* Return to space button for superusers */}
|
||||
@@ -337,10 +406,34 @@ const Menu: FC<{
|
||||
onClick={goToSpace}
|
||||
className="rb-border-t rb:pt-5! rb:pb-2.5! rb:absolute rb:bottom-2.5 rb:right-5 rb:left-5 rb:text-[13px] rb:text-[#5B6167] rb:hover:text-[#212332] rb:leading-4.5 rb:font-regular rb:text-center rb:mt-2.25 rb:cursor-pointer"
|
||||
>
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"></div>
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/logout_grey.svg')]"></div>
|
||||
{collapsed ? null : t('common.returnToSpace')}
|
||||
</Flex>
|
||||
}
|
||||
{source === 'manage' && subscription && !collapsed &&
|
||||
<div className="rb:absolute rb:bottom-3 rb:left-3 rb:right-3 rb:py-3 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/package_bg.png')] rb:overflow-hidden rb:rounded-xl">
|
||||
<div className="rb:h-4.5 rb:flex-1 rb:truncate rb:px-3 rb:text-[13px] rb:font-medium rb:leading-4.5">{subscription.package_plan?.[getKeyWithLanguage('name')]}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-4 rb:mt-4">
|
||||
{['workspace_quota', 'skill_quota', 'app_quota', 'model_quota'].map(key => (
|
||||
<div key={key} className="rb:text-center">
|
||||
<div className="rb:text-[13px] rb:font-[MiSans-Semibold] rb:font-semibold">{subscription.quota?.[key as keyof typeof subscription.quota]}</div>
|
||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[10px] rb:leading-3.5">{t(`index.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Flex align="center" justify="center" className="rb:mt-4! rb:border rb:p-2! rb:text-[12px] rb:leading-4 rb:mx-3! rb:rounded-lg rb:cursor-pointer"
|
||||
onClick={handleViewDetail}
|
||||
>
|
||||
{t('package.viewDetail')}
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/index/arrow_right_dark.svg')]"></div>
|
||||
</Flex>
|
||||
</div>
|
||||
}
|
||||
|
||||
<SubscriptionDetailModal
|
||||
ref={subscriptionDetailRef}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ import { type FC, type ReactNode } from 'react'
|
||||
/** Props interface for Tag component */
|
||||
export interface TagProps {
|
||||
/** Color theme for the tag */
|
||||
color?: 'processing' | 'error' | 'success' | 'warning' | 'default',
|
||||
color?: 'processing' | 'error' | 'success' | 'warning' | 'default' | 'purple' | 'dark',
|
||||
/** Tag content */
|
||||
children: ReactNode;
|
||||
/** Additional CSS classes */
|
||||
@@ -32,6 +32,8 @@ const colors = {
|
||||
success: 'rb:text-[#369F21] rb:border-[rgba(54,159,33,0.25)] rb:bg-[rgba(54,159,33,0.06)]',
|
||||
warning: 'rb:text-[#FF5D34] rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)]',
|
||||
default: 'rb:text-[#5B6167] rb:border-[rgba(91,97,103,0.30)] rb:bg-[rgba(91,97,103,0.08)]',
|
||||
purple: 'rb:text-[#9C6FFF] rb:border-[rgba(156,111,255,0.25)] rb:bg-[rgba(156,111,255,0.06)]',
|
||||
dark: 'rb:text-[#171719] rb:border-[rgba(23,23,25,0.25)] rb:bg-[rgba(23,23,25,0.06)]'
|
||||
}
|
||||
|
||||
/** Custom tag component with color themes */
|
||||
|
||||
Reference in New Issue
Block a user