style: command viewer scroller
This commit is contained in:
@@ -10,6 +10,7 @@ interface CodeViewerProps {
|
|||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
xScrollable?: boolean;
|
||||||
}
|
}
|
||||||
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
@@ -19,7 +20,8 @@ const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
autodetect,
|
autodetect,
|
||||||
ignoreIllegals,
|
ignoreIllegals,
|
||||||
copyable,
|
copyable,
|
||||||
height = 'auto'
|
height = 'auto',
|
||||||
|
xScrollable = false
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,6 +35,7 @@ const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
autodetect={autodetect}
|
autodetect={autodetect}
|
||||||
ignoreIllegals={ignoreIllegals}
|
ignoreIllegals={ignoreIllegals}
|
||||||
copyable={copyable}
|
copyable={copyable}
|
||||||
|
xScrollable={xScrollable}
|
||||||
></CodeViewer>
|
></CodeViewer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface CodeViewerProps {
|
|||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
xScrollable?: boolean;
|
||||||
}
|
}
|
||||||
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
@@ -20,7 +21,8 @@ const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
ignoreIllegals,
|
ignoreIllegals,
|
||||||
copyable,
|
copyable,
|
||||||
style,
|
style,
|
||||||
height = 'auto'
|
height = 'auto',
|
||||||
|
xScrollable = false
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -34,6 +36,7 @@ const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
autodetect={autodetect}
|
autodetect={autodetect}
|
||||||
ignoreIllegals={ignoreIllegals}
|
ignoreIllegals={ignoreIllegals}
|
||||||
copyable={copyable}
|
copyable={copyable}
|
||||||
|
xScrollable={xScrollable}
|
||||||
></CodeViewer>
|
></CodeViewer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface CodeViewerProps {
|
|||||||
height?: string | number;
|
height?: string | number;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
xScrollable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodeHeaderProps {
|
interface CodeHeaderProps {
|
||||||
@@ -43,6 +44,14 @@ const CodeHeaderWrapper = styled.div`
|
|||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
border-radius: var(--border-radius-mini);
|
border-radius: var(--border-radius-mini);
|
||||||
|
&:hover {
|
||||||
|
.custome-scrollbar {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CodeHeader: React.FC<CodeHeaderProps> = ({
|
const CodeHeader: React.FC<CodeHeaderProps> = ({
|
||||||
@@ -80,7 +89,8 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
ignoreIllegals = true,
|
ignoreIllegals = true,
|
||||||
copyable = true,
|
copyable = true,
|
||||||
height = 'auto',
|
height = 'auto',
|
||||||
style
|
style,
|
||||||
|
xScrollable = false
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
|
||||||
const highlightedCode = useMemo(() => {
|
const highlightedCode = useMemo(() => {
|
||||||
@@ -131,7 +141,8 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
|
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
|
||||||
{
|
{
|
||||||
dark: props.theme === 'dark',
|
dark: props.theme === 'dark',
|
||||||
light: props.theme === 'light'
|
light: props.theme === 'light',
|
||||||
|
'x-scrollable': xScrollable
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
@@ -141,7 +152,10 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
style={{ minHeight: height }}
|
style={{
|
||||||
|
minHeight: height,
|
||||||
|
...(xScrollable ? { width: 'max-content' } : {})
|
||||||
|
}}
|
||||||
className={classNames(highlightedCode.className, {
|
className={classNames(highlightedCode.className, {
|
||||||
dark: props.theme === 'dark',
|
dark: props.theme === 'dark',
|
||||||
light: props.theme === 'light'
|
light: props.theme === 'light'
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const HighlightCode: React.FC<{
|
|||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
fixedTheme?: 'light' | 'dark';
|
fixedTheme?: 'light' | 'dark';
|
||||||
|
xScrollable?: boolean;
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
copyValue?: string;
|
copyValue?: string;
|
||||||
@@ -21,7 +22,8 @@ const HighlightCode: React.FC<{
|
|||||||
lang = 'bash',
|
lang = 'bash',
|
||||||
copyable = true,
|
copyable = true,
|
||||||
theme,
|
theme,
|
||||||
height = 'auto'
|
height = 'auto',
|
||||||
|
xScrollable = false
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { userSettings } = useUserSettings();
|
const { userSettings } = useUserSettings();
|
||||||
@@ -40,6 +42,7 @@ const HighlightCode: React.FC<{
|
|||||||
copyValue={copyValue}
|
copyValue={copyValue}
|
||||||
copyable={copyable}
|
copyable={copyable}
|
||||||
height={height}
|
height={height}
|
||||||
|
xScrollable={xScrollable}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -50,6 +53,7 @@ const HighlightCode: React.FC<{
|
|||||||
copyValue={copyValue}
|
copyValue={copyValue}
|
||||||
copyable={copyable}
|
copyable={copyable}
|
||||||
height={height}
|
height={height}
|
||||||
|
xScrollable={xScrollable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
|||||||
// import './iconfont/iconfont.js';
|
// import './iconfont/iconfont.js';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_jo5x7z2z718.js'
|
scriptUrl: '//at.alicdn.com/t/c/font_4613488_rajpu0blzkp.js'
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Tag
|
<Tag
|
||||||
className="more"
|
className="more"
|
||||||
|
variant="outlined"
|
||||||
style={{ marginInline: hiddenIndices.end < 1 ? 0 : `${gap}px 0` }}
|
style={{ marginInline: hiddenIndices.end < 1 ? 0 : `${gap}px 0` }}
|
||||||
ref={moreBtnRef}
|
ref={moreBtnRef}
|
||||||
>
|
>
|
||||||
@@ -132,4 +133,4 @@ const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(TagsWrapper);
|
export default TagsWrapper;
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import {
|
|||||||
DiscordOutlined,
|
DiscordOutlined,
|
||||||
GithubOutlined,
|
GithubOutlined,
|
||||||
HomeOutlined,
|
HomeOutlined,
|
||||||
LogoutOutlined,
|
ReadOutlined
|
||||||
ReadOutlined,
|
|
||||||
SettingOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { history, useIntl, useNavigate } from '@umijs/max';
|
import { history, useIntl, useNavigate } from '@umijs/max';
|
||||||
import { Avatar, Button, Divider, Modal } from 'antd';
|
import { Avatar, Button, Divider, Modal } from 'antd';
|
||||||
@@ -235,7 +233,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: (
|
label: (
|
||||||
<span className="flex flex-center">
|
<span className="flex flex-center">
|
||||||
<SettingOutlined />
|
<IconFont type="icon-settings-02" />
|
||||||
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
||||||
{intl?.formatMessage?.({ id: 'common.button.settings' })}
|
{intl?.formatMessage?.({ id: 'common.button.settings' })}
|
||||||
</span>
|
</span>
|
||||||
@@ -270,7 +268,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
{originNode}
|
{originNode}
|
||||||
<Divider style={{ marginBlock: 4 }} />
|
<Divider style={{ marginBlock: 4 }} />
|
||||||
<CustomItem onClick={handleLogout} className="border-top">
|
<CustomItem onClick={handleLogout} className="border-top">
|
||||||
<LogoutOutlined rotate={180} />
|
<IconFont type="icon-logout" style={{ fontSize: 17 }} />
|
||||||
<span>{intl?.formatMessage?.({ id: 'common.button.logout' })}</span>
|
<span>{intl?.formatMessage?.({ id: 'common.button.logout' })}</span>
|
||||||
</CustomItem>
|
</CustomItem>
|
||||||
</DropdownWrapper>
|
</DropdownWrapper>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
'clusters.workerpool.volumes.add': 'Add Volume',
|
'clusters.workerpool.volumes.add': 'Add Volume',
|
||||||
'clusters.create.provider.self': 'Self-Hosted',
|
'clusters.create.provider.self': 'Self-Hosted',
|
||||||
'clusters.create.provider.cloud': 'Cloud Provider',
|
'clusters.create.provider.cloud': 'Cloud Provider',
|
||||||
'clusters.create.selectProvider': 'Select Provider',
|
'clusters.create.steps.selectProvider': 'Select Provider',
|
||||||
'clusters.create.configBasic': 'Basic Configuration',
|
'clusters.create.configBasic': 'Basic Configuration',
|
||||||
'clusters.create.execCommand': 'Execute Command',
|
'clusters.create.execCommand': 'Execute Command',
|
||||||
'clusters.create.supportedGpu': 'Supported GPUs',
|
'clusters.create.supportedGpu': 'Supported GPUs',
|
||||||
@@ -117,8 +117,9 @@ export default {
|
|||||||
'clusters.form.setDefault': 'Set as Default',
|
'clusters.form.setDefault': 'Set as Default',
|
||||||
'clusters.form.setDefault.tips': 'Default for deployment.',
|
'clusters.form.setDefault.tips': 'Default for deployment.',
|
||||||
'clusters.addworker.noClusters': 'No available clusters',
|
'clusters.addworker.noClusters': 'No available clusters',
|
||||||
'clusters.create.complete.tips': 'Cluster created successfully!',
|
'clusters.create.steps.complete.tips': 'Cluster created successfully!',
|
||||||
'clusters.create.complete': 'Completed',
|
'clusters.create.steps.complete': 'Complete',
|
||||||
|
'clusters.create.steps.configure': 'Configure',
|
||||||
'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
||||||
'clusters.create.dockerTips2':
|
'clusters.create.dockerTips2':
|
||||||
'You can also skip this step and add them later from the cluster list.',
|
'You can also skip this step and add them later from the cluster list.',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
'clusters.workerpool.volumes.add': 'Add Volume',
|
'clusters.workerpool.volumes.add': 'Add Volume',
|
||||||
'clusters.create.provider.self': 'Self-Hosted',
|
'clusters.create.provider.self': 'Self-Hosted',
|
||||||
'clusters.create.provider.cloud': 'Cloud Provider',
|
'clusters.create.provider.cloud': 'Cloud Provider',
|
||||||
'clusters.create.selectProvider': 'Select Provider',
|
'clusters.create.steps.selectProvider': 'Select Provider',
|
||||||
'clusters.create.configBasic': 'Basic Configuration',
|
'clusters.create.configBasic': 'Basic Configuration',
|
||||||
'clusters.create.execCommand': 'Execute Command',
|
'clusters.create.execCommand': 'Execute Command',
|
||||||
'clusters.create.supportedGpu': 'Supported GPUs',
|
'clusters.create.supportedGpu': 'Supported GPUs',
|
||||||
@@ -117,8 +117,9 @@ export default {
|
|||||||
'clusters.form.setDefault': 'Set as Default',
|
'clusters.form.setDefault': 'Set as Default',
|
||||||
'clusters.form.setDefault.tips': 'Default for deployment.',
|
'clusters.form.setDefault.tips': 'Default for deployment.',
|
||||||
'clusters.addworker.noClusters': 'No available clusters',
|
'clusters.addworker.noClusters': 'No available clusters',
|
||||||
'clusters.create.complete.tips': 'Cluster created successfully!',
|
'clusters.create.steps.complete.tips': 'Cluster created successfully!',
|
||||||
'clusters.create.complete': 'Completed',
|
'clusters.create.steps.complete': 'Complete',
|
||||||
|
'clusters.create.steps.configure': 'Configure',
|
||||||
'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
||||||
'clusters.create.dockerTips2':
|
'clusters.create.dockerTips2':
|
||||||
'You can also skip this step and add them later from the cluster list.',
|
'You can also skip this step and add them later from the cluster list.',
|
||||||
@@ -157,7 +158,7 @@ export default {
|
|||||||
// 27. 'clusters.workerpool.volumes.add': 'Add Volume'
|
// 27. 'clusters.workerpool.volumes.add': 'Add Volume'
|
||||||
// 28. 'clusters.create.provider.self': 'Self-Hosted',
|
// 28. 'clusters.create.provider.self': 'Self-Hosted',
|
||||||
// 29. 'clusters.create.provider.cloud': 'Cloud Provider',
|
// 29. 'clusters.create.provider.cloud': 'Cloud Provider',
|
||||||
// 30. 'clusters.create.selectProvider': 'Select Provider',
|
// 30. 'clusters.create.steps.selectProvider': 'Select Provider',
|
||||||
// 31. 'clusters.create.configBasic': 'Basic Configuration',
|
// 31. 'clusters.create.configBasic': 'Basic Configuration',
|
||||||
// 32. 'clusters.create.execCommand': 'Execute Command',
|
// 32. 'clusters.create.execCommand': 'Execute Command',
|
||||||
// 33. 'clusters.create.supportedGpu': 'Supported GPUs',
|
// 33. 'clusters.create.supportedGpu': 'Supported GPUs',
|
||||||
@@ -219,10 +220,11 @@ export default {
|
|||||||
// 85. 'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
|
// 85. 'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
|
||||||
// 86. 'clusters.addworker.enterWorkerAddress.error': 'Please enter the worker external address.',
|
// 86. 'clusters.addworker.enterWorkerAddress.error': 'Please enter the worker external address.',
|
||||||
// 87. 'clusters.addworker.noClusters': 'No available clusters',
|
// 87. 'clusters.addworker.noClusters': 'No available clusters',
|
||||||
// 88. 'clusters.create.complete.tips': 'Cluster created successfully!',
|
// 88. 'clusters.create.steps.complete.tips': 'Cluster created successfully!',
|
||||||
// 89. 'clusters.create.complete': 'Completed',
|
// 89. 'clusters.create.steps.complete': 'Complete',
|
||||||
// 90. 'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
// 90. 'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
||||||
// 91. 'clusters.create.dockerTips2': 'You can also skip this step and add them later from the cluster list.',
|
// 91. 'clusters.create.dockerTips2': 'You can also skip this step and add them later from the cluster list.',
|
||||||
// 92. 'clusters.create.k8sTips1': 'Next, register existing Kubernetes cluster.',
|
// 92. 'clusters.create.k8sTips1': 'Next, register existing Kubernetes cluster.',
|
||||||
// 93. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.'
|
// 93. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.',
|
||||||
|
// 94. 'clusters.create.steps.configure': 'Configure',
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
'clusters.workerpool.volumes.add': 'Добавить том',
|
'clusters.workerpool.volumes.add': 'Добавить том',
|
||||||
'clusters.create.provider.self': 'Самостоятельное размещение',
|
'clusters.create.provider.self': 'Самостоятельное размещение',
|
||||||
'clusters.create.provider.cloud': 'Облачный провайдер',
|
'clusters.create.provider.cloud': 'Облачный провайдер',
|
||||||
'clusters.create.selectProvider': 'Выберите провайдера',
|
'clusters.create.steps.selectProvider': 'Выберите провайдера',
|
||||||
'clusters.create.configBasic': 'Базовая конфигурация',
|
'clusters.create.configBasic': 'Базовая конфигурация',
|
||||||
'clusters.create.execCommand': 'Выполнить команду',
|
'clusters.create.execCommand': 'Выполнить команду',
|
||||||
'clusters.create.supportedGpu': 'Поддерживаемые GPU',
|
'clusters.create.supportedGpu': 'Поддерживаемые GPU',
|
||||||
@@ -118,8 +118,9 @@ export default {
|
|||||||
'clusters.form.setDefault.tips':
|
'clusters.form.setDefault.tips':
|
||||||
'Использовать по умолчанию для развертывания.',
|
'Использовать по умолчанию для развертывания.',
|
||||||
'clusters.addworker.noClusters': 'No available clusters',
|
'clusters.addworker.noClusters': 'No available clusters',
|
||||||
'clusters.create.complete.tips': 'Cluster created successfully!',
|
'clusters.create.steps.complete.tips': 'Cluster created successfully!',
|
||||||
'clusters.create.complete': 'Completed',
|
'clusters.create.steps.complete': 'Complete',
|
||||||
|
'clusters.create.steps.configure': 'Configure',
|
||||||
'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
||||||
'clusters.create.dockerTips2':
|
'clusters.create.dockerTips2':
|
||||||
'You can also skip this step and add them later from the cluster list.',
|
'You can also skip this step and add them later from the cluster list.',
|
||||||
@@ -131,10 +132,11 @@ export default {
|
|||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
// 1. 'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
// 1. 'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
||||||
// 2. 'clusters.addworker.noClusters': 'No available clusters',
|
// 2. 'clusters.addworker.noClusters': 'No available clusters',
|
||||||
// 3. 'clusters.create.complete.tips': 'Cluster created successfully!',
|
// 3. 'clusters.create.steps.complete.tips': 'Cluster created successfully!',
|
||||||
// 4. 'clusters.create.complete': 'Completed',
|
// 4. 'clusters.create.steps.complete': 'Complete',
|
||||||
// 5. 'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
// 5. 'clusters.create.dockerTips1': 'Next, add worker to this cluster.',
|
||||||
// 6. 'clusters.create.dockerTips2': 'You can also skip this step and add them later from the cluster list.',
|
// 6. 'clusters.create.dockerTips2': 'You can also skip this step and add them later from the cluster list.',
|
||||||
// 7. 'clusters.create.k8sTips1': 'Next, register existing Kubernetes cluster.',
|
// 7. 'clusters.create.k8sTips1': 'Next, register existing Kubernetes cluster.',
|
||||||
// 8. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.'
|
// 8. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.',
|
||||||
|
// 9. 'clusters.create.steps.configure': 'Configure',
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
'clusters.workerpool.volumes.add': '添加存储卷',
|
'clusters.workerpool.volumes.add': '添加存储卷',
|
||||||
'clusters.create.provider.self': '自建环境',
|
'clusters.create.provider.self': '自建环境',
|
||||||
'clusters.create.provider.cloud': '云环境',
|
'clusters.create.provider.cloud': '云环境',
|
||||||
'clusters.create.selectProvider': '选择环境',
|
'clusters.create.steps.selectProvider': '选择环境',
|
||||||
'clusters.create.configBasic': '基本配置',
|
'clusters.create.configBasic': '基本配置',
|
||||||
'clusters.create.execCommand': '执行命令',
|
'clusters.create.execCommand': '执行命令',
|
||||||
'clusters.create.supportedGpu': '支持的 GPU',
|
'clusters.create.supportedGpu': '支持的 GPU',
|
||||||
@@ -114,8 +114,9 @@ export default {
|
|||||||
'clusters.form.setDefault': '设为默认',
|
'clusters.form.setDefault': '设为默认',
|
||||||
'clusters.form.setDefault.tips': '部署时的默认集群。',
|
'clusters.form.setDefault.tips': '部署时的默认集群。',
|
||||||
'clusters.addworker.noClusters': '无可用集群',
|
'clusters.addworker.noClusters': '无可用集群',
|
||||||
'clusters.create.complete.tips': '集群创建成功!',
|
'clusters.create.steps.complete.tips': '集群创建成功!',
|
||||||
'clusters.create.complete': '完成',
|
'clusters.create.steps.complete': '完成',
|
||||||
|
'clusters.create.steps.configure': '配置',
|
||||||
'clusters.create.dockerTips1': '接下来,为该集群添加节点。',
|
'clusters.create.dockerTips1': '接下来,为该集群添加节点。',
|
||||||
'clusters.create.dockerTips2': '你也可以跳过此步骤,稍后在集群列表中添加。',
|
'clusters.create.dockerTips2': '你也可以跳过此步骤,稍后在集群列表中添加。',
|
||||||
'clusters.create.k8sTips1': '接下来,注册已有的 Kubernetes 集群。',
|
'clusters.create.k8sTips1': '接下来,注册已有的 Kubernetes 集群。',
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ const CollapseInner = styled(Collapse)`
|
|||||||
border-radius: var(--border-radius-base) !important;
|
border-radius: var(--border-radius-base) !important;
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
|
&:hover {
|
||||||
background-color: var(--ant-color-fill-tertiary) !important;
|
background-color: var(--ant-color-fill-tertiary) !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-collapse-body {
|
.ant-collapse-body {
|
||||||
padding-inline: 0 !important;
|
padding-inline: 0 !important;
|
||||||
@@ -32,7 +34,8 @@ const CollapsePanel: React.FC<{
|
|||||||
activeKey: string | string[];
|
activeKey: string | string[];
|
||||||
accordion?: boolean;
|
accordion?: boolean;
|
||||||
onChange?: (key: string | string[]) => void;
|
onChange?: (key: string | string[]) => void;
|
||||||
}> = ({ items, activeKey, accordion, onChange }) => {
|
styles?: Record<string, React.CSSProperties>;
|
||||||
|
}> = ({ items, activeKey, accordion, onChange, styles }) => {
|
||||||
return (
|
return (
|
||||||
<CollapseInner
|
<CollapseInner
|
||||||
expandIconPlacement="start"
|
expandIconPlacement="start"
|
||||||
@@ -42,6 +45,12 @@ const CollapsePanel: React.FC<{
|
|||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
destroyOnHidden={false}
|
destroyOnHidden={false}
|
||||||
|
styles={{
|
||||||
|
...styles,
|
||||||
|
header: {
|
||||||
|
backgroundColor: 'var(--ant-collapse-header-bg)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
expandIcon={({ isActive }) => (
|
expandIcon={({ isActive }) => (
|
||||||
<IconFont
|
<IconFont
|
||||||
type="icon-down"
|
type="icon-down"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const Header = styled.div`
|
|||||||
padding-inline: 10px;
|
padding-inline: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-bottom: 1px solid var(--ant-color-border);
|
border-bottom: 1px solid var(--ant-color-border);
|
||||||
background-color: var(--ant-color-fill-tertiary);
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ViewerProps {
|
interface ViewerProps {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { clusterSessionAtom } from '@/atoms/clusters';
|
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import ColumnWrapper from '@/pages/_components/column-wrapper';
|
import ColumnWrapper from '@/pages/_components/column-wrapper';
|
||||||
import { useIntl, useSearchParams } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -40,29 +38,20 @@ const Content = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StepsWrapper = styled.div`
|
|
||||||
width: 220px;
|
|
||||||
padding-inline: 24px;
|
|
||||||
padding-block: 16px;
|
|
||||||
border-right: 1px solid var(--ant-color-split);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MainWrapper = styled.div`
|
const MainWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ClusterCreate: React.FC<{
|
const ClusterCreate: React.FC<{
|
||||||
|
action: PageActionType;
|
||||||
|
setCurrentTitle?: (title: string) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}> = ({ onClose }) => {
|
}> = ({ onClose, action, setCurrentTitle }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const startStep = 0;
|
const startStep = 0;
|
||||||
const stepList = useStepList();
|
const stepList = useStepList();
|
||||||
const { systemConfig } = useSystemConfig();
|
const { systemConfig } = useSystemConfig();
|
||||||
const [searchParams] = useSearchParams();
|
const intl = useIntl();
|
||||||
const action =
|
|
||||||
(searchParams.get('action') as PageActionType) || PageAction.CREATE;
|
|
||||||
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
|
|
||||||
const [credentialList, setCredentialList] = useState<
|
const [credentialList, setCredentialList] = useState<
|
||||||
Global.BaseOption<number, { provider: ProviderType }>[]
|
Global.BaseOption<number, { provider: ProviderType }>[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -85,7 +74,6 @@ const ClusterCreate: React.FC<{
|
|||||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||||
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
|
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
|
||||||
const [isAddWorkerStep, setIsAddWorkerStep] = useState<boolean>(false);
|
const [isAddWorkerStep, setIsAddWorkerStep] = useState<boolean>(false);
|
||||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const formRefs: Record<string, any> = {
|
const formRefs: Record<string, any> = {
|
||||||
[moduleMap.BasicForm]: useRef<any>(null),
|
[moduleMap.BasicForm]: useRef<any>(null),
|
||||||
@@ -240,6 +228,7 @@ const ClusterCreate: React.FC<{
|
|||||||
// add worker or cluster registration step
|
// add worker or cluster registration step
|
||||||
const handleAddWorker = () => {
|
const handleAddWorker = () => {
|
||||||
setIsAddWorkerStep(true);
|
setIsAddWorkerStep(true);
|
||||||
|
setCurrentTitle?.(intl.formatMessage({ id: 'resources.button.create' }));
|
||||||
onNext();
|
onNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +290,6 @@ const ClusterCreate: React.FC<{
|
|||||||
const res = await createCluster({ data });
|
const res = await createCluster({ data });
|
||||||
const info = await queryClusterToken({ id: res.id });
|
const info = await queryClusterToken({ id: res.id });
|
||||||
setSubmitLoading(false);
|
setSubmitLoading(false);
|
||||||
setIsComplete(true);
|
|
||||||
setRegistrationInfo({
|
setRegistrationInfo({
|
||||||
...info,
|
...info,
|
||||||
cluster_id: res.id
|
cluster_id: res.id
|
||||||
@@ -320,11 +308,6 @@ const ClusterCreate: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainWrapper>
|
<MainWrapper>
|
||||||
{/* {!isAddWorkerStep && (
|
|
||||||
<StepsWrapper>
|
|
||||||
<ClusterSteps steps={steps} currentStep={currentStep}></ClusterSteps>
|
|
||||||
</StepsWrapper>
|
|
||||||
)} */}
|
|
||||||
<ColumnWrapper
|
<ColumnWrapper
|
||||||
styles={{
|
styles={{
|
||||||
container: { paddingTop: 16, paddingBottom: 16 }
|
container: { paddingTop: 16, paddingBottom: 16 }
|
||||||
@@ -341,13 +324,6 @@ const ClusterCreate: React.FC<{
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* {!isComplete && (
|
|
||||||
<div style={{ marginBottom: 24 }}>
|
|
||||||
<Title level={5} style={{ marginBottom: 8, fontSize: 14 }}>
|
|
||||||
{steps[currentStep]?.subTitle || steps[currentStep]?.title}
|
|
||||||
</Title>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
{!isAddWorkerStep && (
|
{!isAddWorkerStep && (
|
||||||
<div style={{ marginBottom: 32 }}>
|
<div style={{ marginBottom: 32 }}>
|
||||||
<ClusterSteps
|
<ClusterSteps
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import GSDrawer from '@/components/scroller-modal/gs-drawer';
|
import GSDrawer from '@/components/scroller-modal/gs-drawer';
|
||||||
|
import { PageAction } from '@/config';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ClusterCreate from './cluster-create';
|
import ClusterCreate from './cluster-create';
|
||||||
|
|
||||||
@@ -13,13 +14,14 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
title
|
title
|
||||||
}) => {
|
}) => {
|
||||||
|
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GSDrawer
|
<GSDrawer
|
||||||
title={title}
|
title={currentTitle}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleCancel}
|
onClose={handleCancel}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
@@ -34,7 +36,11 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
footer={false}
|
footer={false}
|
||||||
>
|
>
|
||||||
<ClusterCreate onClose={handleCancel}></ClusterCreate>
|
<ClusterCreate
|
||||||
|
onClose={handleCancel}
|
||||||
|
action={PageAction.CREATE}
|
||||||
|
setCurrentTitle={setCurrentTitle}
|
||||||
|
></ClusterCreate>
|
||||||
</GSDrawer>
|
</GSDrawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
|||||||
code={code}
|
code={code}
|
||||||
copyValue={code}
|
copyValue={code}
|
||||||
lang="bash"
|
lang="bash"
|
||||||
|
xScrollable={true}
|
||||||
></HighlightCode>
|
></HighlightCode>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
|
|||||||
body: {
|
body: {
|
||||||
paddingBlock: 0
|
paddingBlock: 0
|
||||||
},
|
},
|
||||||
wrapper: { width: 865 }
|
wrapper: { width: 700 }
|
||||||
}}
|
}}
|
||||||
footer={false}
|
footer={false}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CheckCircleFilled, CheckCircleOutlined } from '@ant-design/icons';
|
|
||||||
import { Steps } from 'antd';
|
import { Steps } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -9,6 +8,9 @@ const Wrapper = styled.div`
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --steps-item-base-width: 20px; type="panel"
|
||||||
|
*/
|
||||||
const Box = styled.div`
|
const Box = styled.div`
|
||||||
.indicates {
|
.indicates {
|
||||||
color: var(--ant-color-text-tertiary);
|
color: var(--ant-color-text-tertiary);
|
||||||
@@ -16,76 +18,20 @@ const Box = styled.div`
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
.ant-steps {
|
.ant-steps {
|
||||||
--steps-title-font-size: 14px;
|
|
||||||
.ant-steps-item {
|
.ant-steps-item {
|
||||||
--steps-item-base-width: 20px;
|
--steps-item-base-width: 20px;
|
||||||
}
|
}
|
||||||
|
.ant-steps-item-rail-wait {
|
||||||
|
--steps-item-solid-line-color: var(--ant-color-split);
|
||||||
}
|
}
|
||||||
`;
|
&:not(.ant-steps-panel) {
|
||||||
|
.ant-steps-item-finish {
|
||||||
const StepItemWrapper = styled.div`
|
--steps-item-icon-bg-color: var(--ant-color-primary);
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
// border-bottom: 1px solid var(--ant-color-split);
|
|
||||||
.description {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 20px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
line-height: 22px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
.description {
|
|
||||||
.title {
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StepsItem: React.FC<{
|
|
||||||
active: boolean;
|
|
||||||
item: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
}> = ({ active, item }) => {
|
|
||||||
return (
|
|
||||||
<StepItemWrapper className={active ? 'active' : ''}>
|
|
||||||
<div className="icon">
|
|
||||||
{active ? (
|
|
||||||
<CheckCircleFilled
|
|
||||||
style={{ color: 'var(--ant-color-primary)', fontSize: 20 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CheckCircleOutlined
|
|
||||||
style={{ color: 'var(--ant-color-text-disabled)', fontSize: 20 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="description">
|
|
||||||
<div className="title">{item.title}</div>
|
|
||||||
<div className="content">{item.content}</div>
|
|
||||||
</div>
|
|
||||||
</StepItemWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClusterSteps: React.FC<{
|
const ClusterSteps: React.FC<{
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
onChange?: (step: number) => void;
|
onChange?: (step: number) => void;
|
||||||
@@ -94,28 +40,41 @@ const ClusterSteps: React.FC<{
|
|||||||
const { steps, currentStep = 0, onChange } = props;
|
const { steps, currentStep = 0, onChange } = props;
|
||||||
|
|
||||||
const visibleSteps = steps.filter((step) => !step.hideInSteps);
|
const visibleSteps = steps.filter((step) => !step.hideInSteps);
|
||||||
return (
|
|
||||||
<Box>
|
const styles: Record<string, any> = {
|
||||||
<Wrapper>
|
root: {
|
||||||
{/* {visibleSteps.map((item, index) => (
|
'--ant-steps-description-max-width': 'auto'
|
||||||
<StepsItem
|
},
|
||||||
key={index}
|
|
||||||
active={currentStep === index}
|
|
||||||
item={item}
|
|
||||||
></StepsItem>
|
|
||||||
))} */}
|
|
||||||
<Steps
|
|
||||||
current={currentStep}
|
|
||||||
items={visibleSteps}
|
|
||||||
type="panel"
|
|
||||||
styles={{
|
|
||||||
item: {
|
item: {
|
||||||
paddingBlock: 0
|
paddingBlock: 0
|
||||||
},
|
},
|
||||||
|
itemIcon: {
|
||||||
|
fontSize: 0,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
marginInlineStart: 0
|
||||||
|
},
|
||||||
|
itemWrapper: {
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
itemRail: {
|
itemRail: {
|
||||||
borderColor: 'var(--ant-color-split)'
|
borderWidth: 1.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
insetInlineStart: 10,
|
||||||
|
insetInlineEnd: 2,
|
||||||
|
'--steps-horizontal-rail-margin': '12px'
|
||||||
}
|
}
|
||||||
}}
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Wrapper>
|
||||||
|
<Steps
|
||||||
|
current={currentStep}
|
||||||
|
items={visibleSteps}
|
||||||
|
variant="filled"
|
||||||
|
size="small"
|
||||||
|
styles={styles}
|
||||||
></Steps>
|
></Steps>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ const SuccessResult: React.FC<{
|
|||||||
}}
|
}}
|
||||||
icon={
|
icon={
|
||||||
<CheckCircleFilled
|
<CheckCircleFilled
|
||||||
style={{ color: 'var(--ant-color-success)', fontSize: 42 }}
|
style={{ color: 'var(--ant-color-success)', fontSize: 36 }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
status="success"
|
status="success"
|
||||||
title={intl.formatMessage({ id: 'clusters.create.complete.tips' })}
|
title={intl.formatMessage({ id: 'clusters.create.steps.complete.tips' })}
|
||||||
subTitle={provider === ProviderValueMap.Kubernetes ? k8sTips : dockerTips}
|
subTitle={provider === ProviderValueMap.Kubernetes ? k8sTips : dockerTips}
|
||||||
extra={[
|
extra={[
|
||||||
<Flex gap={16} justify="center" key="buttons">
|
<Flex gap={16} justify="center" key="buttons">
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export default function useStepList() {
|
|||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.create.selectProvider' }),
|
title: intl.formatMessage({
|
||||||
|
id: 'clusters.create.steps.selectProvider'
|
||||||
|
}),
|
||||||
content: '',
|
content: '',
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
description:
|
description:
|
||||||
@@ -35,7 +37,7 @@ export default function useStepList() {
|
|||||||
providers: []
|
providers: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.title.config' }),
|
title: intl.formatMessage({ id: 'clusters.create.steps.configure' }),
|
||||||
content: '',
|
content: '',
|
||||||
description:
|
description:
|
||||||
'Set the cluster name, description, or other essential parameters in advanced settings.',
|
'Set the cluster name, description, or other essential parameters in advanced settings.',
|
||||||
@@ -72,7 +74,7 @@ export default function useStepList() {
|
|||||||
beforeNext: handleBack
|
beforeNext: handleBack
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.create.complete' }),
|
title: intl.formatMessage({ id: 'clusters.create.steps.complete' }),
|
||||||
content: '',
|
content: '',
|
||||||
description: '',
|
description: '',
|
||||||
showButtons: (provider?: ProviderType) => {
|
showButtons: (provider?: ProviderType) => {
|
||||||
|
|||||||
@@ -77,9 +77,10 @@ const AdvanceConfig = () => {
|
|||||||
included={true}
|
included={true}
|
||||||
max={modelContextData?.scaled || 163840}
|
max={modelContextData?.scaled || 163840}
|
||||||
tooltip={{}}
|
tooltip={{}}
|
||||||
|
step={1024}
|
||||||
marks={{
|
marks={{
|
||||||
[modelContextData?.native]: 'native',
|
[modelContextData?.scaled]: 'scaled',
|
||||||
[modelContextData?.scaled]: 'scaled'
|
[modelContextData?.native]: 'native'
|
||||||
}}
|
}}
|
||||||
></SealSlider>
|
></SealSlider>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user