Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eedc5b295a | ||
|
|
5ee62f267c | ||
|
|
ba0b2bb2c3 | ||
|
|
c3bdbc200e | ||
|
|
63b06e595e | ||
|
|
27210da13a | ||
|
|
831a760246 | ||
|
|
ae06ad7aae | ||
|
|
b3b819a080 | ||
|
|
aa47c626ba | ||
|
|
79e4a60d52 |
+1
-1
@@ -17,7 +17,7 @@
|
||||
"@ant-design/pro-components": "3.1.0-0",
|
||||
"@antv/g6": "^5.0.51",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@gpustack/core-ui": "^1.0.39",
|
||||
"@gpustack/core-ui": "^1.0.41",
|
||||
"@huggingface/gguf": "^0.1.7",
|
||||
"@huggingface/hub": "^0.15.1",
|
||||
"@huggingface/tasks": "^0.11.6",
|
||||
|
||||
Generated
+5
-5
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.2
|
||||
'@gpustack/core-ui':
|
||||
specifier: ^1.0.39
|
||||
version: 1.0.39(czdvzceysqw7iv6pct2ucnb23e)
|
||||
specifier: ^1.0.41
|
||||
version: 1.0.41(czdvzceysqw7iv6pct2ucnb23e)
|
||||
'@huggingface/gguf':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.18
|
||||
@@ -1481,8 +1481,8 @@ packages:
|
||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||
|
||||
'@gpustack/core-ui@1.0.39':
|
||||
resolution: {integrity: sha512-6NS0TDkBFHK6RipfvLwvDnLZlAoa/yFbHV+xeERAnUb+4t0mDyIQFqvYAeDgrUW/JIZ6WScrm31FDZe2sKiuaw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.39.tgz}
|
||||
'@gpustack/core-ui@1.0.41':
|
||||
resolution: {integrity: sha512-Q4mgdjZpvuE5XDioV7MHoVVHK6k7LNzVkq0zGVln1IzAYW7ldpzQPoj/zARXqGWQLAvHLiO03PfEjiP8cE9fdQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.41.tgz}
|
||||
peerDependencies:
|
||||
'@ant-design/icons': ^6.1.0
|
||||
'@ant-design/pro-components': 3.1.0-0
|
||||
@@ -10802,7 +10802,7 @@ snapshots:
|
||||
|
||||
'@formatjs/intl-utils@2.3.0': {}
|
||||
|
||||
'@gpustack/core-ui@1.0.39(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
'@gpustack/core-ui@1.0.41(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
dependencies:
|
||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.ant-alert-with-description .ant-alert-title {
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@import './table.less';
|
||||
@import './alert.less';
|
||||
|
||||
.m-b-20 {
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -43,6 +43,10 @@ export default {
|
||||
DatePicker: {
|
||||
fontSizeLG: 14
|
||||
},
|
||||
Alert: {
|
||||
withDescriptionPadding: '12px 16px',
|
||||
withDescriptionIconSize: 18
|
||||
},
|
||||
Menu: {
|
||||
iconSize: 16,
|
||||
iconMarginInlineEnd: 12,
|
||||
|
||||
@@ -26,8 +26,8 @@ export default {
|
||||
cellPaddingInline: 16,
|
||||
cellPaddingBlock: 6,
|
||||
cellFontSize: 14,
|
||||
rowSelectedHoverBg: 'rgb(249 249 249)',
|
||||
rowHoverBg: 'rgb(249 249 249)',
|
||||
rowSelectedHoverBg: 'rgb(247 247 247)',
|
||||
rowHoverBg: 'rgb(247 247 247)',
|
||||
rowSelectedBg: 'transparent',
|
||||
headerSortActiveBg: 'transparent',
|
||||
headerSortHoverBg: 'transparent',
|
||||
@@ -47,6 +47,10 @@ export default {
|
||||
DatePicker: {
|
||||
fontSizeLG: 14
|
||||
},
|
||||
Alert: {
|
||||
withDescriptionPadding: '12px 16px',
|
||||
withDescriptionIconSize: 18
|
||||
},
|
||||
Menu: {
|
||||
iconSize: 16,
|
||||
iconMarginInlineEnd: 12,
|
||||
@@ -116,7 +120,7 @@ export default {
|
||||
borderRadiusLG: 6,
|
||||
borderRadius: 4,
|
||||
borderRadiusSM: 3,
|
||||
colorBgContainer: '#fff',
|
||||
colorBgContainer: '#fdfdfd',
|
||||
fontSize: 14,
|
||||
motion: true
|
||||
}
|
||||
|
||||
+1
-15
@@ -21,8 +21,6 @@ html {
|
||||
--color-fill-sider: #f4f5f4;
|
||||
--color-bg-1: #f4f5f4;
|
||||
--color-scroll-bg: #d9d9d9;
|
||||
--color-fill-2: #fff;
|
||||
--color-fill-3: #f3f6fa;
|
||||
--color-logs-bg: #1e1e1e;
|
||||
--color-logs-text: #d4d4d4;
|
||||
--layout-content-blockpadding: 24px;
|
||||
@@ -76,7 +74,6 @@ html {
|
||||
--ant-rate-star-color: #fadb14;
|
||||
--color-fill-spin-bg: rgba(255, 255, 255, 15%);
|
||||
--width-tooltip-max: 420px;
|
||||
--color-bg-tooltip: '#fff';
|
||||
--color-modal-content-bg: rgba(255, 255, 255, 90%);
|
||||
--color-modal-box-shadow: 0 4px 16px rgba(0, 0, 0, 10%);
|
||||
--color-spotlight-bg: rgba(255, 255, 255, 100%);
|
||||
@@ -101,7 +98,6 @@ html[data-theme='realDark'] {
|
||||
--color-editor-dark: #00101f;
|
||||
--color-editor-light: #fafafa;
|
||||
--color-fill-spin-bg: rgba(55, 55, 55, 50%);
|
||||
--color-bg-tooltip: #424242;
|
||||
--color-editor-header-bg: #292929;
|
||||
--color-progress-text: rgba(255, 255, 255, 80%);
|
||||
--color-modal-content-bg: #1f1f1f;
|
||||
@@ -116,19 +112,9 @@ html[data-theme='realDark'] {
|
||||
.ant-result-image {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ant-pro-page-container-affix .ant-pro-page-container-warp {
|
||||
background-color: #292929 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.ant-pro-page-container-affix .ant-pro-page-container-warp {
|
||||
background-color: #fff !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
background-color: #f4f5f6;
|
||||
}
|
||||
|
||||
@@ -211,7 +197,7 @@ body {
|
||||
|
||||
tr > td {
|
||||
border-bottom: none;
|
||||
height: 68px;
|
||||
height: var(--seal-table-row-min-height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Popover, theme } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import type { GlobalToken } from 'antd/es/theme';
|
||||
import React from 'react';
|
||||
|
||||
export interface DualArcGaugeItem {
|
||||
index: number;
|
||||
label?: string;
|
||||
memory: {
|
||||
total: number; // bytes
|
||||
used: number; // bytes — outer arc (semantic color)
|
||||
allocated: number; // bytes — inner arc (blue)
|
||||
};
|
||||
}
|
||||
|
||||
interface DualArcGaugeProps {
|
||||
data: DualArcGaugeItem[];
|
||||
}
|
||||
|
||||
// guage external radius: 20, internal radius: 16, stroke width: 2
|
||||
const CX = 25;
|
||||
const CY = 24;
|
||||
const R_OUT = 20;
|
||||
const R_IN = 16;
|
||||
const SW = 2.5;
|
||||
const L_OUT = Math.PI * R_OUT;
|
||||
const L_IN = Math.PI * R_IN;
|
||||
const arcPath = (r: number) =>
|
||||
`M ${CX - r} ${CY} A ${r} ${r} 0 0 1 ${CX + r} ${CY}`;
|
||||
const P_OUT = arcPath(R_OUT);
|
||||
const P_IN = arcPath(R_IN);
|
||||
|
||||
const clamp01 = (n: number) => Math.min(Math.max(n, 0), 1);
|
||||
|
||||
// Used color: green < 75%, yellow < 90%, red >= 90%
|
||||
const usedColor = (ratio: number, token: GlobalToken) => {
|
||||
if (ratio >= 0.9) return token.colorError;
|
||||
if (ratio >= 0.75) return token.colorWarning;
|
||||
return token.colorSuccess;
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
row: css`
|
||||
width: fit-content;
|
||||
padding: 3px 6px;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
&:hover {
|
||||
background-color: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
index: css`
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${token.colorTextSecondary};
|
||||
border-bottom: 1px dashed var(--ant-blue-6);
|
||||
padding-bottom: 3px;
|
||||
line-height: 1.2;
|
||||
`,
|
||||
popTitle: css`
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${token.colorText};
|
||||
margin-bottom: 6px;
|
||||
`,
|
||||
popList: css`
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
`,
|
||||
metric: css`
|
||||
white-space: nowrap;
|
||||
`,
|
||||
dot: css`
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
`,
|
||||
value: css`
|
||||
min-width: 64px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
label: css`
|
||||
color: ${token.colorTextTertiary};
|
||||
`
|
||||
}));
|
||||
|
||||
const DualArcGauge: React.FC<DualArcGaugeProps> = ({ data }) => {
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyles();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const trackColor = token.colorFillSecondary;
|
||||
const allocColor = 'var(--ant-blue-6)';
|
||||
|
||||
return (
|
||||
<Flex vertical gap={2} style={{ width: 'fit-content' }}>
|
||||
{data.map((item) => {
|
||||
const { total = 0, used = 0, allocated = 0 } = item.memory || {};
|
||||
const usedR = total > 0 ? clamp01(used / total) : 0;
|
||||
const allocR = total > 0 ? clamp01(allocated / total) : 0;
|
||||
const pct = Math.round(usedR * 100);
|
||||
const uc = usedColor(usedR, token);
|
||||
// set dim color for allocated if used is 0, otherwise use the same color as used
|
||||
const allocDotColor =
|
||||
allocated > 0 ? allocColor : token.colorTextQuaternary;
|
||||
|
||||
const content = (
|
||||
<div>
|
||||
<div className={styles.popTitle}>
|
||||
[{item.index}]{item.label ? ` ${item.label}` : ''} ·{' '}
|
||||
{convertFileSize(total)}
|
||||
</div>
|
||||
<Flex vertical gap={4} className={styles.popList}>
|
||||
<Flex align="center" gap={6} className={styles.metric}>
|
||||
<span className={styles.dot} style={{ background: uc }} />
|
||||
<span className={styles.value}>{convertFileSize(used)}</span>
|
||||
<span className={styles.label}>
|
||||
{intl.formatMessage({ id: 'resources.table.used' })}
|
||||
</span>
|
||||
</Flex>
|
||||
<Flex align="center" gap={6} className={styles.metric}>
|
||||
<span
|
||||
className={styles.dot}
|
||||
style={{ background: allocDotColor }}
|
||||
/>
|
||||
<span className={styles.value}>
|
||||
{convertFileSize(allocated)}
|
||||
</span>
|
||||
<span className={styles.label}>
|
||||
{intl.formatMessage({ id: 'resources.table.allocated' })}
|
||||
</span>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
key={item.index}
|
||||
content={content}
|
||||
placement="right"
|
||||
trigger={['hover', 'click']}
|
||||
>
|
||||
<Flex align="center" gap={6} className={styles.row}>
|
||||
<svg
|
||||
viewBox="0 0 50 28"
|
||||
width={54}
|
||||
height={30}
|
||||
style={{ overflow: 'visible', flexShrink: 0 }}
|
||||
>
|
||||
<path
|
||||
d={P_OUT}
|
||||
fill="none"
|
||||
strokeWidth={SW}
|
||||
stroke={trackColor}
|
||||
/>
|
||||
<path
|
||||
d={P_IN}
|
||||
fill="none"
|
||||
strokeWidth={SW}
|
||||
stroke={trackColor}
|
||||
/>
|
||||
{usedR > 0 && (
|
||||
<path
|
||||
d={P_OUT}
|
||||
fill="none"
|
||||
strokeWidth={SW}
|
||||
stroke={uc}
|
||||
strokeDasharray={`${(usedR * L_OUT).toFixed(1)} ${L_OUT.toFixed(1)}`}
|
||||
/>
|
||||
)}
|
||||
{allocR > 0 && (
|
||||
<path
|
||||
d={P_IN}
|
||||
fill="none"
|
||||
strokeWidth={SW}
|
||||
style={{ stroke: allocColor }}
|
||||
strokeDasharray={`${(allocR * L_IN).toFixed(1)} ${L_IN.toFixed(1)}`}
|
||||
/>
|
||||
)}
|
||||
<text
|
||||
x={CX}
|
||||
y={CY - 0.5}
|
||||
textAnchor="middle"
|
||||
fontSize={9.5}
|
||||
fontWeight={500}
|
||||
fill={uc}
|
||||
style={{ fontVariantNumeric: 'tabular-nums' }}
|
||||
>
|
||||
{pct}%
|
||||
</text>
|
||||
</svg>
|
||||
{<span className={styles.index}>[{item.index}]</span>}
|
||||
</Flex>
|
||||
</Popover>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default DualArcGauge;
|
||||
@@ -120,13 +120,19 @@ const CommunityBackends: React.FC<{
|
||||
|
||||
const renderItem = (item: ListItem) => {
|
||||
return (
|
||||
<BackendCard
|
||||
active={currentData?.id === item.id}
|
||||
data={item}
|
||||
layout="community"
|
||||
onClick={handleOnClickItem}
|
||||
actionsRenderer={actionsRenderer}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--ant-color-bg-container)'
|
||||
}}
|
||||
>
|
||||
<BackendCard
|
||||
active={currentData?.id === item.id}
|
||||
data={item}
|
||||
layout="community"
|
||||
onClick={handleOnClickItem}
|
||||
actionsRenderer={actionsRenderer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
background-color: var(--ant-color-bg-elevated);
|
||||
font-weight: 600;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Tag } from 'antd';
|
||||
@@ -271,6 +272,10 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
{cpuManufacturer}
|
||||
</ThemeTag>
|
||||
)}
|
||||
<PluginExtraFields
|
||||
name="InstanceTypeBillingBadge"
|
||||
context={{ instanceType: item }}
|
||||
/>
|
||||
</Flex>
|
||||
</Title>
|
||||
<InstanceMetadataSection spec={specData}></InstanceMetadataSection>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
|
||||
import { TemplateCard } from '@gpustack/core-ui';
|
||||
import { Empty, Flex, Spin } from 'antd';
|
||||
@@ -40,6 +41,10 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
|
||||
return (
|
||||
<Flex orientation="vertical" gap={16}>
|
||||
<PluginExtraFields
|
||||
name="InstanceTypeBillingProvider"
|
||||
context={{ instanceTypes: dataList }}
|
||||
/>
|
||||
{dataList.map((item) => {
|
||||
const name = item.name;
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Dropdown,
|
||||
DropdownProps,
|
||||
Empty,
|
||||
Flex,
|
||||
Form,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
@@ -80,9 +81,8 @@ const Label = styled.div`
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
margin-block: 8px 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
`;
|
||||
|
||||
interface AccessControlFormProps {
|
||||
@@ -107,6 +107,14 @@ type AllowedUsersOverride = {
|
||||
routeId?: number;
|
||||
action: PageActionType;
|
||||
}>;
|
||||
// Optional trigger (e.g. an "Add" button) rendered by the host in its
|
||||
// own layout slot while `Field` renders the body. Same props as Field
|
||||
// so the plugin can gate visibility on action/routeId.
|
||||
Action?: React.ComponentType<{
|
||||
form: any;
|
||||
routeId?: number;
|
||||
action: PageActionType;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Extra policy entries prepended to the radio list (rendered before
|
||||
@@ -407,41 +415,56 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
||||
</Tooltip>
|
||||
</Label>
|
||||
|
||||
<Form.Item<AccessControlFormData> name="access_policy" noStyle>
|
||||
<Radio.Group
|
||||
onChange={handleOnPolicyChange}
|
||||
style={{ marginBottom: 12 }}
|
||||
options={[
|
||||
...prependedPolicies.map((p) => ({
|
||||
label: intl.formatMessage({ id: p.labelId }),
|
||||
value: p.policyValue
|
||||
})),
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.accessSettings.authed' }),
|
||||
value: 'authed'
|
||||
},
|
||||
allowedUsersOverride
|
||||
? {
|
||||
label: intl.formatMessage({
|
||||
id: allowedUsersOverride.labelId
|
||||
}),
|
||||
value: allowedUsersOverride.policyValue
|
||||
}
|
||||
: {
|
||||
label: intl.formatMessage({
|
||||
id: 'models.accessSettings.allowedUsers'
|
||||
}),
|
||||
value: ALLOWED_PRINCIPALS_POLICY
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'models.accessSettings.public'
|
||||
}),
|
||||
value: 'public'
|
||||
}
|
||||
]}
|
||||
></Radio.Group>
|
||||
</Form.Item>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{ marginBottom: 16, height: 40 }}
|
||||
>
|
||||
<Form.Item<AccessControlFormData> name="access_policy" noStyle>
|
||||
<Radio.Group
|
||||
onChange={handleOnPolicyChange}
|
||||
options={[
|
||||
...prependedPolicies.map((p) => ({
|
||||
label: intl.formatMessage({ id: p.labelId }),
|
||||
value: p.policyValue
|
||||
})),
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'models.accessSettings.authed'
|
||||
}),
|
||||
value: 'authed'
|
||||
},
|
||||
allowedUsersOverride
|
||||
? {
|
||||
label: intl.formatMessage({
|
||||
id: allowedUsersOverride.labelId
|
||||
}),
|
||||
value: allowedUsersOverride.policyValue
|
||||
}
|
||||
: {
|
||||
label: intl.formatMessage({
|
||||
id: 'models.accessSettings.allowedUsers'
|
||||
}),
|
||||
value: ALLOWED_PRINCIPALS_POLICY
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'models.accessSettings.public'
|
||||
}),
|
||||
value: 'public'
|
||||
}
|
||||
]}
|
||||
></Radio.Group>
|
||||
</Form.Item>
|
||||
{allowedUsersOverride?.Action &&
|
||||
accessPolicy === overridePolicyValue && (
|
||||
<allowedUsersOverride.Action
|
||||
form={form}
|
||||
routeId={currentData?.id}
|
||||
action={action}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{accessPolicy === 'public' && (
|
||||
<div style={{ marginBlock: '16px 12px' }}>
|
||||
<AlertBlockInfo
|
||||
@@ -449,6 +472,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
||||
message={intl.formatMessage({
|
||||
id: 'models.accessSettings.public.tips'
|
||||
})}
|
||||
overlayScrollerProps={{
|
||||
styles: {
|
||||
wrapper: {
|
||||
paddingLeft: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
></AlertBlockInfo>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -130,7 +130,9 @@ const AccessControlModal: React.FC<
|
||||
<AlertBlockInfo
|
||||
type="warning"
|
||||
style={{ marginBottom: 16 }}
|
||||
contentStyle={{ paddingInline: 0 }}
|
||||
contentStyle={{
|
||||
paddingInline: 0
|
||||
}}
|
||||
message={intl.formatMessage({
|
||||
id: 'models.button.accessSettings.tips'
|
||||
})}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import {
|
||||
IconFont,
|
||||
StatusTag,
|
||||
StatusDot,
|
||||
TagsWrapper,
|
||||
TemplateCard,
|
||||
ThemeTag
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -192,6 +193,23 @@ const ModelItem: React.FC<{
|
||||
return _.round(max_tokens / 1024);
|
||||
}, [model]);
|
||||
|
||||
// Ready is the normal state — show just the dot (no label); other states
|
||||
// (Not Ready / Stopped) keep their label so the problem is legible.
|
||||
const statusNode = (
|
||||
<StatusDot
|
||||
statusValue={{
|
||||
status: MyModelsStatusMap[model.status],
|
||||
text:
|
||||
model.status === MyModelsStatusValueMap.Ready ||
|
||||
!MyModelsStatusLabelMap[model.status]
|
||||
? ''
|
||||
: intl.formatMessage({
|
||||
id: MyModelsStatusLabelMap[model.status]
|
||||
})
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<CardWrapper>
|
||||
<TemplateCard
|
||||
@@ -210,18 +228,32 @@ const ModelItem: React.FC<{
|
||||
) : (
|
||||
<ModelLogo src={defaultModelLogo} alt="" />
|
||||
)}
|
||||
<span>{model.name}</span>
|
||||
<span className="flex-center" style={{ gap: 8, minWidth: 0 }}>
|
||||
<span>{model.name}</span>
|
||||
{/* Status moved to a compact dot right after the name, freeing
|
||||
the header's right side for the price summary. The dot keeps
|
||||
the state message on hover (StatusDot has no built-in one). */}
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
fontSize: 'var(--font-size-small)'
|
||||
}}
|
||||
>
|
||||
{model.state_message ? (
|
||||
<Tooltip title={model.state_message}>
|
||||
<span style={{ display: 'inline-flex' }}>
|
||||
{statusNode}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
statusNode
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex-center gap-8">
|
||||
<PluginExtraFields name="ModelPriceSummary" context={{ model }} />
|
||||
</span>
|
||||
<StatusTag
|
||||
maxTooltipWidth={400}
|
||||
statusValue={{
|
||||
status: MyModelsStatusMap[model.status],
|
||||
text: intl.formatMessage({
|
||||
id: MyModelsStatusLabelMap[model.status] || ''
|
||||
}),
|
||||
message: model.state_message
|
||||
}}
|
||||
></StatusTag>
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
@@ -268,6 +269,10 @@ const UserModels: React.FC = () => {
|
||||
</Space>
|
||||
}
|
||||
></PageTools>
|
||||
<PluginExtraFields
|
||||
name="ModelBillingProvider"
|
||||
context={{ models: dataList }}
|
||||
/>
|
||||
<InfiniteScrollerProvider
|
||||
value={{
|
||||
total: dataSource.totalPage,
|
||||
|
||||
@@ -177,6 +177,7 @@ const ModelItem: React.FC<ModelItemProps> = ({
|
||||
<span>
|
||||
<AutoComplete
|
||||
loading={loading}
|
||||
allowClear
|
||||
showSearch={{
|
||||
filterOption: (inputValue, option: any) => {
|
||||
return (
|
||||
|
||||
@@ -300,6 +300,7 @@ const Workers: React.FC<WorkersProps> = ({ clusterId, source }) => {
|
||||
></FilterBar>
|
||||
<ConfigProvider renderEmpty={renderEmpty}>
|
||||
<Table
|
||||
tableLayout="auto"
|
||||
columns={columns}
|
||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||
showSorterTooltip={false}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { Tooltip } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import semverCoerce from 'semver/functions/coerce';
|
||||
import semverGt from 'semver/functions/gt';
|
||||
import { status, WorkerStatusMap, WorkerStatusMapValue } from '../config';
|
||||
@@ -143,15 +143,57 @@ const GPUCell = ({ devices }: { devices: GPUDeviceItem[] }) => (
|
||||
</span>
|
||||
);
|
||||
|
||||
// index + ProgressBar
|
||||
const VRAMItem = ({
|
||||
item,
|
||||
autoOpen
|
||||
}: {
|
||||
item: GPUDeviceItem;
|
||||
autoOpen: boolean;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(autoOpen);
|
||||
}, [autoOpen]);
|
||||
|
||||
const percent = item.memory?.used
|
||||
? _.round(item.memory?.utilization_rate, 0)
|
||||
: _.round((item.memory?.allocated / item.memory?.total) * 100, 0);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={<InfoColumn fieldList={fieldList} data={item.memory} />}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<span className="flex-center" style={{ cursor: 'pointer' }}>
|
||||
<span
|
||||
className="m-r-5"
|
||||
style={{ display: 'flex', width: 25, lineHeight: 1.2 }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
paddingBottom: 2,
|
||||
borderBottom: '1px dashed var(--ant-blue-6)'
|
||||
}}
|
||||
>
|
||||
[{item.index}]
|
||||
</span>
|
||||
</span>
|
||||
<ProgressBar percent={percent} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const VRAMCell = ({
|
||||
devices,
|
||||
intl,
|
||||
rIndex,
|
||||
loadend,
|
||||
firstLoad
|
||||
}: {
|
||||
devices: GPUDeviceItem[];
|
||||
intl: any;
|
||||
rIndex: number;
|
||||
loadend: boolean;
|
||||
firstLoad: boolean;
|
||||
@@ -160,36 +202,11 @@ const VRAMCell = ({
|
||||
{_.map(
|
||||
_.sortBy(devices || [], ['index']),
|
||||
(item: GPUDeviceItem, index: number) => (
|
||||
<span key={index} className="flex-center">
|
||||
<span
|
||||
className="m-r-5"
|
||||
style={{ display: 'flex', width: 25, lineHeight: 1 }}
|
||||
>
|
||||
[{item.index}]
|
||||
</span>
|
||||
<ProgressBar
|
||||
defaultOpen={rIndex === 0 && index === 0 && loadend && firstLoad}
|
||||
percent={
|
||||
item.memory?.used
|
||||
? _.round(item.memory?.utilization_rate, 0)
|
||||
: _.round(
|
||||
(item.memory?.allocated / item.memory?.total) * 100,
|
||||
0
|
||||
)
|
||||
}
|
||||
label={<InfoColumn fieldList={fieldList} data={item.memory} />}
|
||||
/>
|
||||
{item.memory.is_unified_memory && (
|
||||
<Tooltip
|
||||
title={intl.formatMessage({ id: 'resources.table.unified' })}
|
||||
>
|
||||
<InfoCircleOutlined
|
||||
className="m-l-5"
|
||||
style={{ color: 'var(--ant-blue-5)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
<VRAMItem
|
||||
key={index}
|
||||
item={item}
|
||||
autoOpen={rIndex === 0 && index === 0 && loadend && firstLoad}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
@@ -498,6 +515,7 @@ const useWorkerColumns = ({
|
||||
{
|
||||
title: 'GPU',
|
||||
dataIndex: 'gpu',
|
||||
minWidth: 100,
|
||||
render: (_, record) =>
|
||||
statusAvailable(record) ? (
|
||||
<GPUCell devices={record?.status?.gpu_devices} />
|
||||
@@ -508,11 +526,11 @@ const useWorkerColumns = ({
|
||||
{
|
||||
title: intl.formatMessage({ id: 'resources.table.vram' }),
|
||||
dataIndex: 'vram',
|
||||
minWidth: 100,
|
||||
render: (_, record, rIndex) =>
|
||||
statusAvailable(record) ? (
|
||||
<VRAMCell
|
||||
devices={record?.status?.gpu_devices}
|
||||
intl={intl}
|
||||
rIndex={rIndex}
|
||||
loadend={loadend}
|
||||
firstLoad={firstLoad}
|
||||
@@ -524,6 +542,7 @@ const useWorkerColumns = ({
|
||||
{
|
||||
title: intl.formatMessage({ id: 'resources.table.disk' }),
|
||||
dataIndex: 'storage',
|
||||
minWidth: 100,
|
||||
render: (_, record) => (
|
||||
<span className="flex-center flex-full">
|
||||
{statusAvailable(record) ? (
|
||||
|
||||
Reference in New Issue
Block a user