fix: gpu service issues

This commit is contained in:
jialin
2026-05-26 14:21:52 +08:00
committed by jialin
parent 88ab927755
commit 95e03d11c1
20 changed files with 187 additions and 256 deletions
+1 -1
View File
@@ -284,6 +284,6 @@ export default {
'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Remaining {count}',
'common.max': 'Maximum {count}',
'common.max': 'Max {count}',
'common.validate.group': 'Please complete the {group} configuration'
};
+3 -2
View File
@@ -16,8 +16,8 @@ export default {
'gpuservice.template.containerDisk': 'Container Disk (GB)',
'gpuservice.template.memory': 'Memory (GB)',
'gpuservice.instance.containerDisk.remaining':
'Container Disk (Remaining {count} GB)',
'gpuservice.instance.memory.remaining': 'Memory (Remaining {count} GB)',
'Container Disk (Max {count} GB)',
'gpuservice.instance.memory.remaining': 'Memory (Max {count} GB)',
'gpuservice.template.displayName': 'Display Name',
'gpuservice.template.displayName.max':
'Display name cannot exceed 63 characters.',
@@ -99,6 +99,7 @@ export default {
'gpuservice.instance.sliced': 'Sliced',
'gpuservice.instance.memory': 'Memory',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk',
'gpuservice.instance.search.type.placeholder': 'Search by name',
'gpuservice.instance.search.template.placeholder':
'Search by template name, image or mount path',
+1 -1
View File
@@ -284,7 +284,7 @@ export default {
'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': '残り {count}',
'common.max': 'Maximum {count}',
'common.max': '最大 {count}',
'common.validate.group': 'Please complete the {group} configuration'
};
+3 -2
View File
@@ -16,8 +16,8 @@ export default {
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
'gpuservice.template.memory': 'メモリ (GB)',
'gpuservice.instance.containerDisk.remaining':
'コンテナディスク (残り {count} GB)',
'gpuservice.instance.memory.remaining': 'メモリ (残り {count} GB)',
'コンテナディスク (最大 {count} GB)',
'gpuservice.instance.memory.remaining': 'メモリ (最大 {count} GB)',
'gpuservice.template.displayName': '表示名',
'gpuservice.template.displayName.max':
'表示名は 63 文字以内で入力してください。',
@@ -98,6 +98,7 @@ export default {
'gpuservice.instance.sliced': '分割',
'gpuservice.instance.memory': 'Memory',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'ディスク',
'gpuservice.instance.search.type.placeholder': '名前で検索',
'gpuservice.instance.search.template.placeholder':
'テンプレート名、イメージまたはマウントパスで検索',
+1 -1
View File
@@ -283,7 +283,7 @@ export default {
'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Остаток {count}',
'common.max': 'Maximum {count}',
'common.max': 'Макс. {count}',
'common.validate.group': 'Please complete the {group} configuration'
};
+3 -2
View File
@@ -17,8 +17,8 @@ export default {
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
'gpuservice.template.memory': 'Память (GB)',
'gpuservice.instance.containerDisk.remaining':
'Диск контейнера (Остаток {count} GB)',
'gpuservice.instance.memory.remaining': 'Память (Остаток {count} GB)',
'Диск контейнера (Макс. {count} GB)',
'gpuservice.instance.memory.remaining': 'Память (Макс. {count} GB)',
'gpuservice.template.displayName': 'Отображаемое имя',
'gpuservice.template.displayName.max':
'Отображаемое имя не должно превышать 63 символа.',
@@ -99,6 +99,7 @@ export default {
'gpuservice.instance.sliced': 'Разделено',
'gpuservice.instance.memory': 'Память',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Диск',
'gpuservice.instance.search.type.placeholder': 'Поиск по имени',
'gpuservice.instance.search.template.placeholder':
'Поиск по имени шаблона, образу или пути монтирования',
+1 -1
View File
@@ -287,6 +287,6 @@ export default {
'common.image.limit.width': 'Image width must be {width}.',
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Kalan {count}',
'common.max': 'Maksimum {count}',
'common.max': 'Maks. {count}',
'common.validate.group': 'Please complete the {group} configuration'
};
+3 -2
View File
@@ -16,8 +16,8 @@ export default {
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
'gpuservice.template.memory': 'Bellek (GB)',
'gpuservice.instance.containerDisk.remaining':
'Konteyner Diski (Kalan {count} GB)',
'gpuservice.instance.memory.remaining': 'Bellek (Kalan {count} GB)',
'Konteyner Diski (Maks. {count} GB)',
'gpuservice.instance.memory.remaining': 'Bellek (Maks. {count} GB)',
'gpuservice.template.displayName': 'Görünen Ad',
'gpuservice.template.displayName.max':
'Görünen ad 63 karakterden uzun olamaz.',
@@ -98,6 +98,7 @@ export default {
'gpuservice.instance.sliced': 'Bölünmüş',
'gpuservice.instance.memory': 'Bellek',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk',
'gpuservice.instance.search.type.placeholder': 'Ada göre ara',
'gpuservice.instance.search.template.placeholder':
'Şablon adına, imaja veya bağlama yoluna göre ara',
+1
View File
@@ -92,6 +92,7 @@ export default {
'gpuservice.instance.sliced': '切分',
'gpuservice.instance.memory': '显存',
'gpuservice.instance.ram': '内存',
'gpuservice.instance.disk': '磁盘',
'gpuservice.instance.search.type.placeholder': '搜索名称',
'gpuservice.instance.search.template.placeholder':
'搜索模板名称、镜像或挂载路径',
@@ -46,13 +46,9 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
{ length: Math.max(0, maxCount) },
(_, i) => i + 1
);
const computedItems = showCustomInput
? presetItems.filter((n) => n >= min && n <= max)
: Array.from(
{ length: Math.max(0, Math.floor((max - min) / step) + 1) },
(_, i) => min + i * step
);
const items = computedItems.length === 0 ? [0] : computedItems;
const items = presetItems;
const isItemDisabled = (num: number) =>
!!disabled || num > max || num < min;
const [inputValue, setInputValue] = useState<number | null>(() =>
value !== undefined && value !== null && !presetItems.includes(value)
? value
@@ -70,7 +66,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
}, [value]);
const handleSelect = (num: number) => {
if (disabled || num === value) {
if (isItemDisabled(num) || num === value) {
return;
}
setInputValue(null);
@@ -108,15 +104,17 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
<Flex className={styles.contentWrapper} align="center">
<div className={styles.content}>
{items.map((num) => {
const itemDisabled = isItemDisabled(num);
return (
<div
key={num}
role="radio"
aria-checked={num === value}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
aria-disabled={itemDisabled}
tabIndex={itemDisabled ? -1 : 0}
className={classNames(styles.numberItem, {
[styles.active]: num === value && !!value
[styles.active]: num === value && !!value,
[styles.itemDisabled]: itemDisabled && !disabled
})}
onClick={() => handleSelect(num)}
onKeyDown={(e) => {
@@ -85,6 +85,13 @@
font-weight: 500;
border-color: var(--ant-color-split);
}
&.itemDisabled {
cursor: not-allowed;
color: var(--ant-color-text-disabled);
&:hover {
background-color: transparent;
}
}
}
&.disabled {
cursor: not-allowed;
@@ -80,7 +80,6 @@ const MetaItem: React.FC<{
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const intl = useIntl();
const name = item.name;
const specData = item.spec || {};
const acceleratable = specData.acceleratable;
@@ -93,11 +92,9 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const remainingData = item.status?.remaining || {};
const renderName = () => {
const product = specData.product;
const cpuCapacity = remainingData.cpu;
const displayName =
product ||
(!acceleratable && cpuCapacity ? `${cpuCapacity} vCPUs` : name);
const displayName = specData.acceleratable
? specData.product || item.name
: 'CPU';
return displayName;
};
@@ -162,7 +159,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
value={toDisplayUnit(convertKiToGi(remainingData.ram)) ?? '-'}
></MetaItem>
<MetaItem
show={acceleratable}
show={!!remainingData.cpu}
showDot={true}
icon="icon-cpu"
label="vCPU"
@@ -1,6 +1,6 @@
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
import { PageActionType } from '@/config/types';
import { Input as CInput } from '@gpustack/core-ui';
import { Input as CInput, useAppUtils } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
@@ -15,6 +15,7 @@ const Basic = ({
disabled?: boolean;
}) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return (
<>
<OwnerPrincipalIdField name="owner_principal_id" />
@@ -24,9 +25,7 @@ const Basic = ({
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.instance.name.required'
})
message: getRuleMessage('input', 'common.table.name')
},
{
pattern: validateLabelNameRegxFor63,
@@ -36,7 +35,7 @@ const Basic = ({
>
<CInput.Input
disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
@@ -540,10 +540,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
rules={[
{
required: true,
message: getRuleMessage(
'select',
'gpuservice.instance.ssh.assignKey'
)
message: getRuleMessage('select', 'gpuservice.publicKey')
}
]}
>
@@ -97,9 +97,9 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
fontWeight: 400
}}
>
{description.manufacturer
{description.acceleratable
? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
: ''}
: 'CPU'}
</span>
</Flex>
</SelectedCard>
@@ -185,7 +185,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
></Alert>
)
}
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
label={`${intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} (${intl.formatMessage(
{
id: 'common.max'
},
{ count: maxGpuCount }
)})`}
/>
</Form.Item>
)}
@@ -2,7 +2,6 @@ import { PageAction } from '@/config';
import {
Input as CInput,
InputNumber as CInputNumber,
CheckboxField,
LabelInfo,
Select,
useAppUtils
@@ -14,21 +13,17 @@ import { useEffect, useMemo, useState } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types';
import useCreateStorage from '../../storage/services/use-create-storage';
import useQueryStorage from '../../storage/services/use-query-storage';
import useQueryStorageTypes from '../../storage-types/services/use-query-storage-types';
import { DEFAULT_PV_CAPACITY_GB, StorageModeValueMap } from '../config';
import { StorageModeValueMap } from '../config';
import { FormData } from '../config/types';
import StorageOverlay from './storage-overlay';
const DEFAULT_TEMP_CAPACITY_GB = 50;
const detectMode = (volume?: FormData['spec']['volume']) => {
if (volume?.persistentTemplate) {
return { mode: StorageModeValueMap.Persistent, releaseWithInstance: true };
if (volume?.persistent || volume?.persistentTemplate) {
return StorageModeValueMap.Persistent;
}
if (volume?.persistent) {
return { mode: StorageModeValueMap.Persistent, releaseWithInstance: false };
}
return { mode: StorageModeValueMap.Temporary, releaseWithInstance: false };
return StorageModeValueMap.Temporary;
};
const StorageVolume = ({
@@ -45,25 +40,19 @@ const StorageVolume = ({
const [storageMode, setStorageMode] = useState<string>(
StorageModeValueMap.Temporary
);
const [releaseWithInstance, setReleaseWithInstance] = useState<boolean>(false);
useEffect(() => {
if (!currentVolume) return;
const detected = detectMode(currentVolume);
setStorageMode(detected.mode);
setReleaseWithInstance(detected.releaseWithInstance);
setStorageMode(detectMode(currentVolume));
}, [currentVolume]);
const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } =
useQueryStorage();
const { detailData: storageTypesData, fetchData: fetchStorageTypes } =
useQueryStorageTypes();
const [overlayOpen, setOverlayOpen] = useState(false);
useEffect(() => {
fetchStorage({ page: 1, perPage: 100 });
fetchStorageTypes({ page: 1, perPage: 100 });
}, []);
const storageOptions = useMemo(
@@ -75,43 +64,19 @@ const StorageVolume = ({
[storageData]
);
const storageTypeOptions = useMemo(
() =>
(storageTypesData?.items || []).map((item) => ({
label: item.displayName || item.name,
value: item.name
})),
[storageTypesData]
);
const applyMode = (mode: string, release: boolean) => {
const applyMode = (mode: string) => {
if (mode === StorageModeValueMap.Temporary) {
form.setFieldValue(['spec', 'volume'], {
ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` }
});
return;
}
if (release) {
form.setFieldValue(['spec', 'volume'], {
persistentTemplate: {
spec: { type: '', capacity: `${DEFAULT_PV_CAPACITY_GB}Gi` },
releaseWithInstance: true
}
});
return;
}
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } });
};
const handleModeChange = (mode: string) => {
setStorageMode(mode);
applyMode(mode, releaseWithInstance);
};
const handleReleaseChange = (e: any) => {
const next = !!e?.target?.checked;
setReleaseWithInstance(next);
applyMode(storageMode, next);
applyMode(mode);
};
const handleCreateStorage = async (values: StorageFormData) => {
@@ -130,48 +95,60 @@ const StorageVolume = ({
return (
<>
<div data-field="storage"></div>
<Radio.Group
disabled={disabled}
style={{ marginBottom: 12 }}
value={storageMode}
onChange={(e) => handleModeChange(e.target.value)}
options={[
{
label: (
<LabelInfo
description={intl.formatMessage({
id: 'gpuservice.storage.temporary.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.temporary'
})}
</span>
}
/>
),
value: StorageModeValueMap.Temporary
},
{
label: (
<LabelInfo
description={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
</span>
}
/>
),
value: StorageModeValueMap.Persistent
}
]}
/>
<Flex align="center" justify="space-between" style={{ marginBlock: 16 }}>
<Radio.Group
disabled={disabled}
value={storageMode}
style={{ display: 'flex', gap: 12 }}
onChange={(e) => handleModeChange(e.target.value)}
options={[
{
label: (
<LabelInfo
description={intl.formatMessage({
id: 'gpuservice.storage.temporary.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.temporary'
})}
</span>
}
/>
),
value: StorageModeValueMap.Temporary
},
{
label: (
<LabelInfo
description={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
</span>
}
/>
),
value: StorageModeValueMap.Persistent
}
]}
/>
{action === PageAction.CREATE &&
storageMode === StorageModeValueMap.Persistent && (
<Button
type="link"
size="small"
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
)}
</Flex>
{storageMode === StorageModeValueMap.Temporary && (
<Form.Item
@@ -205,106 +182,26 @@ const StorageVolume = ({
{storageMode === StorageModeValueMap.Persistent && (
<>
<Flex
style={{
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12
}}
<Form.Item
name={['spec', 'volume', 'persistent', 'name']}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<CheckboxField
<Select
disabled={disabled}
checked={releaseWithInstance}
onChange={handleReleaseChange}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.releaseWithInstance'
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
{action === PageAction.CREATE && !releaseWithInstance && (
<Button
type="link"
size="small"
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
)}
</Flex>
{releaseWithInstance ? (
<>
<Form.Item
name={['spec', 'volume', 'persistentTemplate', 'spec', 'type']}
rules={[
{
required: true,
message: getRuleMessage('select', 'gpuservice.storageType')
}
]}
>
<Select
required
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.storageType' })}
options={storageTypeOptions}
/>
</Form.Item>
<Form.Item
name={[
'spec',
'volume',
'persistentTemplate',
'spec',
'capacity'
]}
getValueProps={(val) => ({
value: val
? Number(String(val).replace(/Gi$/i, '')) || undefined
: undefined
})}
normalize={(val) => (val ? `${val}Gi` : '')}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity.required'
})
}
]}
>
<CInputNumber
required
disabled={disabled}
min={1}
precision={0}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity'
})}
/>
</Form.Item>
</>
) : (
<Form.Item
name={['spec', 'volume', 'persistent', 'name']}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<Select
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
</Form.Item>
)}
</Form.Item>
</>
)}
@@ -13,6 +13,7 @@ import _ from 'lodash';
import { Fragment, useMemo } from 'react';
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
import { ListItem } from '../config/types';
import tableSyles from '../styles/table.module.less';
type ConnectEntry =
| {
@@ -66,27 +67,6 @@ const getConnectEntries = (record: ListItem): ConnectEntry[] => {
});
};
const renderInstanceType = (record: ListItem) => {
const description = JSON.parse(record?.description || '{}').spec || {};
return (
<Flex align="flex-start" orientation="vertical">
<span
style={{
color: 'var(--ant-color-text)',
fontWeight: 400
}}
>
{description.manufacturer
? `${description.product} x ${record.spec?.resources?.accelerator}`
: ''}
</span>
<span className="text-tertiary">
{`${record.spec.resources?.cpu}C / RAM: ${record.spec.resources?.ram?.replace('Gi', 'GB')} / Disk: ${record.spec.resources?.localStorage?.replace('Gi', 'GB')}`}{' '}
</span>
</Flex>
);
};
interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[];
@@ -97,6 +77,36 @@ const useInstancesColumns = ({
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
const renderInstanceType = (record: ListItem) => {
const description = JSON.parse(record?.description || '{}').spec || {};
return (
<Flex align="flex-start" orientation="vertical">
<span className="text-primary">
{description.acceleratable
? `${description.product} x ${record.spec?.resources?.accelerator}`
: 'CPU'}
</span>
<Flex
align="center"
style={{ fontSize: 13, color: 'var(--ant-color-text-tertiary)' }}
>
<span>{record.spec.resources?.cpu}C</span>
<span className={tableSyles.dot} />
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}:{' '}
{record.spec.resources?.ram?.replace('Gi', 'GB')}
</span>
<span className={tableSyles.dot} />
<span>
{intl.formatMessage({ id: 'gpuservice.instance.disk' })}:{' '}
{record.spec.resources?.localStorage?.replace('Gi', 'GB')}
</span>
</Flex>
</Flex>
);
};
return useMemo(() => {
return [
{
@@ -116,6 +126,9 @@ const useInstancesColumns = ({
{
title: intl.formatMessage({ id: 'gpuservice.instance.connect' }),
key: 'connect',
ellipsis: {
showTitle: false
},
render: (_text, record: ListItem) => {
const entries = getConnectEntries(record);
@@ -199,6 +212,9 @@ const useInstancesColumns = ({
dataIndex: ['status', 'phase'],
key: 'status',
sorter: false,
ellipsis: {
showTitle: false
},
render: (value: string, record: ListItem) => {
return (
<StatusTag
@@ -212,7 +228,7 @@ const useInstancesColumns = ({
}
},
{
title: 'Instance Type',
title: intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
dataIndex: ['spec', 'type'],
key: 'type',
sorter: false,
@@ -220,12 +236,9 @@ const useInstancesColumns = ({
showTitle: false
},
render: (_text: string, record: ListItem) => (
<AutoTooltip
ghost
style={{ maxWidth: 360, color: 'var(--ant-color-text) !important' }}
>
{renderInstanceType(record)}
</AutoTooltip>
<div style={{ width: 'max-content' }}>
<AutoTooltip ghost>{renderInstanceType(record)}</AutoTooltip>
</div>
)
},
{
@@ -246,6 +259,9 @@ const useInstancesColumns = ({
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
ellipsis: {
showTitle: false
},
render: (_text, record) => (
<DropdownButtons
items={rowActionList}
@@ -0,0 +1,9 @@
.dot {
display: flex;
width: 3px;
height: 3px;
margin-inline: 5px;
border-radius: 50%;
background-color: var(--ant-color-text-quaternary);
flex: none;
}
@@ -150,6 +150,7 @@ const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({
<div className={styles.port}>
<CInputNumber
min={PORT_MIN}
controls={false}
disabled={disabled}
precision={0}
value={item.port}
@@ -2,17 +2,17 @@
display: flex;
gap: 12px;
width: 100%;
flex-wrap: wrap;
// flex-wrap: wrap;
.select {
flex: 1 1 120px;
min-width: 120px;
flex: 1 1 100px;
min-width: 100px;
}
.port {
flex: 1 1 120px;
min-width: 120px;
flex: 1 1 100px;
min-width: 100px;
}
.name {
flex: 100 1 180px;
min-width: 180px;
flex: 100 1 160px;
min-width: 160px;
}
}