feat(gpu-service): use grouped flavor select in instance type create drawer
This commit is contained in:
@@ -138,6 +138,8 @@ export default {
|
|||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'gpuservice.instanceType.flavor': 'Flavor',
|
||||||
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
||||||
|
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||||
|
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||||
'gpuservice.instanceType.product': 'Product',
|
'gpuservice.instanceType.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export default {
|
|||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'gpuservice.instanceType.flavor': 'Flavor',
|
||||||
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
||||||
|
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||||
|
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||||
'gpuservice.instanceType.product': 'Product',
|
'gpuservice.instanceType.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ export default {
|
|||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'gpuservice.instanceType.flavor': 'Flavor',
|
||||||
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
||||||
|
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||||
|
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||||
'gpuservice.instanceType.product': 'Product',
|
'gpuservice.instanceType.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export default {
|
|||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'gpuservice.instanceType.flavor': 'Flavor',
|
||||||
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor',
|
||||||
|
'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute',
|
||||||
|
'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute',
|
||||||
'gpuservice.instanceType.product': 'Product',
|
'gpuservice.instanceType.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ export default {
|
|||||||
'gpuservice.instanceType.add': '添加实例类型',
|
'gpuservice.instanceType.add': '添加实例类型',
|
||||||
'gpuservice.instanceType.flavor': '规格',
|
'gpuservice.instanceType.flavor': '规格',
|
||||||
'gpuservice.instanceType.flavor.required': '请选择实例类型规格',
|
'gpuservice.instanceType.flavor.required': '请选择实例类型规格',
|
||||||
|
'gpuservice.instanceType.flavor.gpuGroup': 'GPU 算力',
|
||||||
|
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 算力',
|
||||||
'gpuservice.instanceType.product': '商品',
|
'gpuservice.instanceType.product': '商品',
|
||||||
'gpuservice.instanceType.unitCpu': '单位 CPU',
|
'gpuservice.instanceType.unitCpu': '单位 CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU',
|
'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU',
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import Separator from '@/pages/llmodels/components/separator';
|
|
||||||
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
|
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Typography, message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FlavorItem, FormData } from '../config/types';
|
import { FlavorItem, FormData } from '../config/types';
|
||||||
import GPUServiceInstanceTypeForm from '../forms';
|
import GPUServiceInstanceTypeForm from '../forms';
|
||||||
import useQueryFlavors from '../services/use-query-flavors';
|
import useQueryFlavors from '../services/use-query-flavors';
|
||||||
import styles from '../styles/instance-types.module.less';
|
|
||||||
import FlavorList from './flavor-list';
|
|
||||||
|
|
||||||
type AddInstanceTypeModalProps = {
|
type AddInstanceTypeModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -18,28 +15,6 @@ type AddInstanceTypeModalProps = {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColTitle: React.FC<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}> = ({ children, style }) => (
|
|
||||||
<Typography.Title
|
|
||||||
level={3}
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingBottom: 16,
|
|
||||||
margin: 0,
|
|
||||||
position: 'sticky',
|
|
||||||
top: 0,
|
|
||||||
zIndex: 100,
|
|
||||||
backgroundColor: 'var(--ant-color-bg-elevated)',
|
|
||||||
...style
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Typography.Title>
|
|
||||||
);
|
|
||||||
|
|
||||||
const AddInstanceTypeModal: React.FC<AddInstanceTypeModalProps> = ({
|
const AddInstanceTypeModal: React.FC<AddInstanceTypeModalProps> = ({
|
||||||
title,
|
title,
|
||||||
open,
|
open,
|
||||||
@@ -101,63 +76,37 @@ const AddInstanceTypeModal: React.FC<AddInstanceTypeModalProps> = ({
|
|||||||
mask={{ closable: false }}
|
mask={{ closable: false }}
|
||||||
keyboard={false}
|
keyboard={false}
|
||||||
styles={{
|
styles={{
|
||||||
wrapper: { width: 'min(900px, calc(100vw - 220px))' },
|
wrapper: { width: 'min(600px, calc(100vw - 220px))' },
|
||||||
body: { overflowY: 'hidden' }
|
body: { overflowY: 'hidden' }
|
||||||
}}
|
}}
|
||||||
footer={false}
|
footer={false}
|
||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<ColumnWrapper
|
||||||
<div className={styles.colWrapper}>
|
styles={{ container: { paddingBlock: 0 } }}
|
||||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
footer={
|
||||||
<div className={styles.panelBody}>
|
<ModalFooter
|
||||||
<div className={styles.stickyHead}>
|
onOk={handleSubmit}
|
||||||
<ColTitle
|
onCancel={handleCancel}
|
||||||
style={{
|
loading={loading}
|
||||||
paddingBottom: 0
|
style={{
|
||||||
}}
|
padding: '16px 24px 8px',
|
||||||
>
|
display: 'flex',
|
||||||
{intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })}
|
justifyContent: 'flex-end'
|
||||||
</ColTitle>
|
}}
|
||||||
</div>
|
/>
|
||||||
<FlavorList
|
}
|
||||||
value={selectedFlavor?.name}
|
>
|
||||||
dataList={flavorList}
|
<GPUServiceInstanceTypeForm
|
||||||
loading={flavorLoading}
|
ref={form}
|
||||||
onChange={setSelectedFlavor}
|
open={open}
|
||||||
/>
|
selectedFlavor={selectedFlavor}
|
||||||
</div>
|
flavorList={flavorList}
|
||||||
</ColumnWrapper>
|
flavorLoading={flavorLoading}
|
||||||
<Separator />
|
onFlavorChange={setSelectedFlavor}
|
||||||
</div>
|
onFinish={onFinish}
|
||||||
<div className={styles.formWrapper}>
|
onFinishFailed={release}
|
||||||
<ColumnWrapper
|
/>
|
||||||
styles={{ container: { paddingBlock: 0 } }}
|
</ColumnWrapper>
|
||||||
footer={
|
|
||||||
<ModalFooter
|
|
||||||
onOk={handleSubmit}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
loading={loading}
|
|
||||||
style={{
|
|
||||||
padding: '16px 24px 8px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ColTitle>
|
|
||||||
{intl.formatMessage({ id: 'common.title.config' })}
|
|
||||||
</ColTitle>
|
|
||||||
<GPUServiceInstanceTypeForm
|
|
||||||
ref={form}
|
|
||||||
open={open}
|
|
||||||
selectedFlavor={selectedFlavor}
|
|
||||||
onFinish={onFinish}
|
|
||||||
onFinishFailed={release}
|
|
||||||
/>
|
|
||||||
</ColumnWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GSDrawer>
|
</GSDrawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { validateLabelNameRegxFor63 } from '@/config';
|
import { validateLabelNameRegxFor63 } from '@/config';
|
||||||
import {
|
import {
|
||||||
|
AutoTooltip,
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select as SealSelect,
|
Select as SealSelect,
|
||||||
|
ThemeTag,
|
||||||
useAppUtils
|
useAppUtils
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Flex, Form } from 'antd';
|
||||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
|
import { formatMemoryDisplay } from '../../instances/config';
|
||||||
|
import { manufactureColorMap } from '../../templates/config';
|
||||||
|
import { formatManufacturer } from '../../utils';
|
||||||
import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config';
|
import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config';
|
||||||
import { FlavorItem, FormData } from '../config/types';
|
import { FlavorItem, FormData } from '../config/types';
|
||||||
|
import styles from '../styles/instance-types.module.less';
|
||||||
|
|
||||||
// RAM / storage are entered as a plain number in GB but stored/submitted as a
|
// RAM / storage are entered as a plain number in GB but stored/submitted as a
|
||||||
// "Gi" quantity string. These drive the FormItem's submit (`normalize`) and
|
// "Gi" quantity string. These drive the FormItem's submit (`normalize`) and
|
||||||
@@ -23,16 +29,39 @@ const giValueProps = (value?: string | null) => ({
|
|||||||
interface InstanceTypeFormProps {
|
interface InstanceTypeFormProps {
|
||||||
ref?: any;
|
ref?: any;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
// The flavor picked in the drawer's first column. Its acceleratorGroup /
|
// The flavor picked from the flavor Select. Its acceleratorGroup /
|
||||||
// generalGroup / acceleratable are copied into the created instance type.
|
// generalGroup / acceleratable are copied into the created instance type.
|
||||||
selectedFlavor?: FlavorItem | null;
|
selectedFlavor?: FlavorItem | null;
|
||||||
|
flavorList: FlavorItem[];
|
||||||
|
flavorLoading?: boolean;
|
||||||
|
onFlavorChange: (flavor: FlavorItem | null) => void;
|
||||||
onFinish: (values: FormData) => Promise<void>;
|
onFinish: (values: FormData) => Promise<void>;
|
||||||
onFinishFailed?: (errorInfo: any) => void;
|
onFinishFailed?: (errorInfo: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A flavor's title mirrors the flavor card: a generic (no product, no/`generic`
|
||||||
|
// manufacturer, non-acceleratable) flavor reads as "CPU-only".
|
||||||
|
const getFlavorTitle = (flavor: FlavorItem) => {
|
||||||
|
const spec = flavor.spec || {};
|
||||||
|
const manufacturer = spec.manufacturer || '';
|
||||||
|
const isCpuOnly =
|
||||||
|
!spec.acceleratable &&
|
||||||
|
!spec.product &&
|
||||||
|
(!manufacturer || manufacturer.toLowerCase() === 'generic');
|
||||||
|
return isCpuOnly ? 'CPU-only' : spec.product || flavor.name || '-';
|
||||||
|
};
|
||||||
|
|
||||||
const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const { open, selectedFlavor, onFinish, onFinishFailed } = props;
|
const {
|
||||||
|
open,
|
||||||
|
selectedFlavor,
|
||||||
|
flavorList,
|
||||||
|
flavorLoading,
|
||||||
|
onFlavorChange,
|
||||||
|
onFinish,
|
||||||
|
onFinishFailed
|
||||||
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const [form] = Form.useForm<FormData>();
|
const [form] = Form.useForm<FormData>();
|
||||||
@@ -68,6 +97,119 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// A flavor's dropdown / selected label shows the same info as the flavor
|
||||||
|
// Secondary line, dot-separated: manufacturer · memory · sliceable. memory
|
||||||
|
// and sliceable apply to accelerator (GPU) flavors only; sliceable stays a
|
||||||
|
// tag. Returns null when a (generic) flavor has nothing to show.
|
||||||
|
const renderFlavorMeta = (flavor: FlavorItem) => {
|
||||||
|
const spec = flavor.spec || {};
|
||||||
|
const manufacturer = spec.manufacturer || '';
|
||||||
|
const color = manufactureColorMap[manufacturer] ?? 'purple';
|
||||||
|
const memory = spec.acceleratable
|
||||||
|
? formatMemoryDisplay(spec.memory ?? undefined)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const pieces: React.ReactNode[] = [];
|
||||||
|
if (manufacturer) {
|
||||||
|
pieces.push(
|
||||||
|
<ThemeTag
|
||||||
|
key="vendor"
|
||||||
|
color={color}
|
||||||
|
style={{ fontWeight: 400, marginInlineEnd: 0 }}
|
||||||
|
>
|
||||||
|
{formatManufacturer(manufacturer)}
|
||||||
|
</ThemeTag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (memory) {
|
||||||
|
pieces.push(<span key="memory">{memory}</span>);
|
||||||
|
}
|
||||||
|
if (spec.acceleratable && spec.sliceable) {
|
||||||
|
pieces.push(
|
||||||
|
<ThemeTag
|
||||||
|
key="sliceable"
|
||||||
|
color="geekblue"
|
||||||
|
style={{ fontWeight: 400, marginInlineEnd: 0 }}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'gpuservice.instance.sliceable' })}
|
||||||
|
</ThemeTag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!pieces.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap={8}
|
||||||
|
style={{
|
||||||
|
minWidth: 0,
|
||||||
|
color: 'var(--ant-color-text-tertiary)',
|
||||||
|
fontSize: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pieces.flatMap((piece, index) =>
|
||||||
|
index === 0
|
||||||
|
? [piece]
|
||||||
|
: [
|
||||||
|
<span
|
||||||
|
key={`dot-${index}`}
|
||||||
|
style={{ color: 'var(--ant-color-text-quaternary)' }}
|
||||||
|
>
|
||||||
|
·
|
||||||
|
</span>,
|
||||||
|
piece
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dropdown item: two lines — name on top, meta row below.
|
||||||
|
const renderFlavorOption = (flavor: FlavorItem) => (
|
||||||
|
<Flex vertical gap={4} style={{ minWidth: 0, padding: '2px 0' }}>
|
||||||
|
<AutoTooltip ghost minWidth={20} maxWidth={'100%'}>
|
||||||
|
{getFlavorTitle(flavor)}
|
||||||
|
</AutoTooltip>
|
||||||
|
{renderFlavorMeta(flavor)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Collapsed selected value: single line — name then meta inline.
|
||||||
|
const renderFlavorSelected = (flavor: FlavorItem) => (
|
||||||
|
<Flex align="center" gap={8} style={{ minWidth: 0 }}>
|
||||||
|
<AutoTooltip ghost minWidth={20} maxWidth={200}>
|
||||||
|
{getFlavorTitle(flavor)}
|
||||||
|
</AutoTooltip>
|
||||||
|
{renderFlavorMeta(flavor)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Split flavors into two groups: CPU compute (generic) and GPU compute
|
||||||
|
// (accelerator). Groups render as labeled sections in the dropdown.
|
||||||
|
const toFlavorOption = (flavor: FlavorItem) => ({
|
||||||
|
value: flavor.name,
|
||||||
|
label: getFlavorTitle(flavor),
|
||||||
|
flavor
|
||||||
|
});
|
||||||
|
const cpuFlavors = flavorList.filter((flavor) => !flavor.spec?.acceleratable);
|
||||||
|
const gpuFlavors = flavorList.filter((flavor) => flavor.spec?.acceleratable);
|
||||||
|
const flavorOptions = [
|
||||||
|
cpuFlavors.length && {
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'gpuservice.instanceType.flavor.cpuGroup'
|
||||||
|
}),
|
||||||
|
title: 'cpu',
|
||||||
|
options: cpuFlavors.map(toFlavorOption)
|
||||||
|
},
|
||||||
|
gpuFlavors.length && {
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'gpuservice.instanceType.flavor.gpuGroup'
|
||||||
|
}),
|
||||||
|
title: 'gpu',
|
||||||
|
options: gpuFlavors.map(toFlavorOption)
|
||||||
|
}
|
||||||
|
].filter(Boolean) as any;
|
||||||
|
|
||||||
const handleFinish = async (values: FormData) => {
|
const handleFinish = async (values: FormData) => {
|
||||||
// The hardware group / acceleratable flags are not user-editable; they
|
// The hardware group / acceleratable flags are not user-editable; they
|
||||||
// come from the chosen flavor. os is fixed to lowercase "linux". ram /
|
// come from the chosen flavor. os is fixed to lowercase "linux". ram /
|
||||||
@@ -117,6 +259,33 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<SealSelect
|
||||||
|
label={intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })}
|
||||||
|
required
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
classNames={{ popup: { root: styles.flavorDropdown } }}
|
||||||
|
loading={flavorLoading}
|
||||||
|
value={selectedFlavor?.name}
|
||||||
|
options={flavorOptions}
|
||||||
|
onChange={(val: string) =>
|
||||||
|
onFlavorChange(
|
||||||
|
flavorList.find((flavor) => flavor.name === val) ?? null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
optionRender={(option: any) =>
|
||||||
|
renderFlavorOption(option.data.flavor)
|
||||||
|
}
|
||||||
|
labelRender={({ value }) => {
|
||||||
|
const flavor = flavorList.find((item) => item.name === value);
|
||||||
|
return flavor
|
||||||
|
? renderFlavorSelected(flavor)
|
||||||
|
: ((value ?? '') as React.ReactNode);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
disabled
|
disabled
|
||||||
|
|||||||
@@ -42,6 +42,24 @@
|
|||||||
min-height: 96px;
|
min-height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ flavor select dropdown ============
|
||||||
|
.flavorDropdown {
|
||||||
|
:global {
|
||||||
|
// tighten the indent of grouped options
|
||||||
|
.ant-select-item-option-grouped {
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
}
|
||||||
|
// divider between options
|
||||||
|
.ant-select-item-option {
|
||||||
|
border-block-end: 1px solid var(--ant-color-border-secondary);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-block-end: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ list card (Linear-style, minimal) ============
|
// ============ list card (Linear-style, minimal) ============
|
||||||
.listCard {
|
.listCard {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user