feat: add instance recreate action
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@antv/g6": "^5.0.51",
|
"@antv/g6": "^5.0.51",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.11",
|
"@gpustack/core-ui": "^1.0.12",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
Generated
+5
-5
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.2
|
version: 7.1.2
|
||||||
'@gpustack/core-ui':
|
'@gpustack/core-ui':
|
||||||
specifier: ^1.0.11
|
specifier: ^1.0.12
|
||||||
version: 1.0.11(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.12(czdvzceysqw7iv6pct2ucnb23e)
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
specifier: ^0.1.7
|
specifier: ^0.1.7
|
||||||
version: 0.1.18
|
version: 0.1.18
|
||||||
@@ -1484,8 +1484,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
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
|
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.11':
|
'@gpustack/core-ui@1.0.12':
|
||||||
resolution: {integrity: sha512-1NpR0oT8e93BAMEtzAR9XZkOGercohG3P/QwIiEclHDaILbReUeGWoT1t1sP5yazu4v7uXRucV4rhPApLQ5gBA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.11.tgz}
|
resolution: {integrity: sha512-2nwQ7i5UXO0Slm2mrZxxx4C8+932xBw2Q9laqZ+OLZscBq7070YEty383MGeujTnIKwrijBcP7L3boCPr8cyWQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.12.tgz}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@ant-design/icons': '>=6.0.0'
|
'@ant-design/icons': '>=6.0.0'
|
||||||
'@ant-design/pro-components': 3.1.0-0
|
'@ant-design/pro-components': 3.1.0-0
|
||||||
@@ -10790,7 +10790,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.11(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.12(czdvzceysqw7iv6pct2ucnb23e)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@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)
|
'@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)
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default {
|
|||||||
'common.button.authorize': 'Role Authorization',
|
'common.button.authorize': 'Role Authorization',
|
||||||
'common.button.confirm': 'Confirm',
|
'common.button.confirm': 'Confirm',
|
||||||
'common.button.viewlog': 'View Logs',
|
'common.button.viewlog': 'View Logs',
|
||||||
|
'common.button.viewevent': 'View Events',
|
||||||
|
'common.button.recreate': 'Recreate',
|
||||||
'common.table.operation': 'Operations',
|
'common.table.operation': 'Operations',
|
||||||
'common.table.createTime': 'Created',
|
'common.table.createTime': 'Created',
|
||||||
'common.table.updateTime': 'Updated',
|
'common.table.updateTime': 'Updated',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default {
|
|||||||
'gpuservice.template.ports.name': 'Name',
|
'gpuservice.template.ports.name': 'Name',
|
||||||
'gpuservice.template.ports.name.max':
|
'gpuservice.template.ports.name.max':
|
||||||
'Port name cannot exceed 16 characters.',
|
'Port name cannot exceed 16 characters.',
|
||||||
|
'gpuservice.template.ports.name.duplicate': 'Port names must be unique.',
|
||||||
'gpuservice.template.env': 'Environment Variables',
|
'gpuservice.template.env': 'Environment Variables',
|
||||||
'gpuservice.template.env.add': 'Add Environment Variable',
|
'gpuservice.template.env.add': 'Add Environment Variable',
|
||||||
'gpuservice.template.env.invalid':
|
'gpuservice.template.env.invalid':
|
||||||
@@ -66,6 +67,16 @@ export default {
|
|||||||
'gpuservice.instance.template.mount': 'Mount',
|
'gpuservice.instance.template.mount': 'Mount',
|
||||||
'gpuservice.instance.connect': 'Connect',
|
'gpuservice.instance.connect': 'Connect',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command',
|
'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command',
|
||||||
|
'gpuservice.instance.event.reason': 'Reason',
|
||||||
|
'gpuservice.instance.event.message': 'Message',
|
||||||
|
'gpuservice.instance.event.source': 'Source',
|
||||||
|
'gpuservice.instance.event.count': 'Count',
|
||||||
|
'gpuservice.instance.event.lastSeen': 'Last Seen',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'Only events from the last hour are shown',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': 'Confirm recreation',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'The current instance will be deleted first, then recreated with the current configuration.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'Storage',
|
'gpuservice.storage': 'Storage',
|
||||||
'gpuservice.storage.add': 'Add Storage',
|
'gpuservice.storage.add': 'Add Storage',
|
||||||
'gpuservice.storage.edit': 'Edit Storage',
|
'gpuservice.storage.edit': 'Edit Storage',
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default {
|
|||||||
'common.button.authorize': 'ロール認可',
|
'common.button.authorize': 'ロール認可',
|
||||||
'common.button.confirm': '確認',
|
'common.button.confirm': '確認',
|
||||||
'common.button.viewlog': 'ログを表示',
|
'common.button.viewlog': 'ログを表示',
|
||||||
|
'common.button.viewevent': 'イベントを表示',
|
||||||
|
'common.button.recreate': '再作成',
|
||||||
'common.table.operation': '操作',
|
'common.table.operation': '操作',
|
||||||
'common.table.createTime': '作成日時',
|
'common.table.createTime': '作成日時',
|
||||||
'common.table.updateTime': '更新日時',
|
'common.table.updateTime': '更新日時',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default {
|
|||||||
'gpuservice.template.ports.name': '名前',
|
'gpuservice.template.ports.name': '名前',
|
||||||
'gpuservice.template.ports.name.max':
|
'gpuservice.template.ports.name.max':
|
||||||
'ポート名は 16 文字以内で入力してください。',
|
'ポート名は 16 文字以内で入力してください。',
|
||||||
|
'gpuservice.template.ports.name.duplicate': 'ポート名は重複できません。',
|
||||||
'gpuservice.template.env': '環境変数',
|
'gpuservice.template.env': '環境変数',
|
||||||
'gpuservice.template.env.add': '環境変数を追加',
|
'gpuservice.template.env.add': '環境変数を追加',
|
||||||
'gpuservice.template.env.invalid': '環境変数を完成させてください。',
|
'gpuservice.template.env.invalid': '環境変数を完成させてください。',
|
||||||
@@ -64,6 +65,16 @@ export default {
|
|||||||
'gpuservice.instance.template.mount': 'マウント',
|
'gpuservice.instance.template.mount': 'マウント',
|
||||||
'gpuservice.instance.connect': '接続',
|
'gpuservice.instance.connect': '接続',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー',
|
'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー',
|
||||||
|
'gpuservice.instance.event.reason': '理由',
|
||||||
|
'gpuservice.instance.event.message': 'メッセージ',
|
||||||
|
'gpuservice.instance.event.source': 'ソース',
|
||||||
|
'gpuservice.instance.event.count': '回数',
|
||||||
|
'gpuservice.instance.event.lastSeen': '最終発生',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'直近 1 時間のイベントのみ表示されます',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'現在のインスタンスを削除した後、現在の構成で再作成します。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'ストレージ',
|
'gpuservice.storage': 'ストレージ',
|
||||||
'gpuservice.storage.add': 'ストレージを追加',
|
'gpuservice.storage.add': 'ストレージを追加',
|
||||||
'gpuservice.storage.edit': 'ストレージを編集',
|
'gpuservice.storage.edit': 'ストレージを編集',
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default {
|
|||||||
'common.button.authorize': 'Настройка прав',
|
'common.button.authorize': 'Настройка прав',
|
||||||
'common.button.confirm': 'Подтвердить',
|
'common.button.confirm': 'Подтвердить',
|
||||||
'common.button.viewlog': 'Просмотр логов',
|
'common.button.viewlog': 'Просмотр логов',
|
||||||
|
'common.button.viewevent': 'Просмотр событий',
|
||||||
|
'common.button.recreate': 'Пересоздать',
|
||||||
'common.table.operation': 'Действия',
|
'common.table.operation': 'Действия',
|
||||||
'common.table.createTime': 'Создано',
|
'common.table.createTime': 'Создано',
|
||||||
'common.table.updateTime': 'Обновлено',
|
'common.table.updateTime': 'Обновлено',
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export default {
|
|||||||
'gpuservice.template.ports.name': 'Имя',
|
'gpuservice.template.ports.name': 'Имя',
|
||||||
'gpuservice.template.ports.name.max':
|
'gpuservice.template.ports.name.max':
|
||||||
'Имя порта не должно превышать 16 символов.',
|
'Имя порта не должно превышать 16 символов.',
|
||||||
|
'gpuservice.template.ports.name.duplicate':
|
||||||
|
'Имена портов должны быть уникальными.',
|
||||||
'gpuservice.template.env': 'Переменные окружения',
|
'gpuservice.template.env': 'Переменные окружения',
|
||||||
'gpuservice.template.env.add': 'Добавить переменную окружения',
|
'gpuservice.template.env.add': 'Добавить переменную окружения',
|
||||||
'gpuservice.template.env.invalid':
|
'gpuservice.template.env.invalid':
|
||||||
@@ -66,6 +68,16 @@ export default {
|
|||||||
'gpuservice.instance.template.mount': 'Монтирование',
|
'gpuservice.instance.template.mount': 'Монтирование',
|
||||||
'gpuservice.instance.connect': 'Подключение',
|
'gpuservice.instance.connect': 'Подключение',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH',
|
'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH',
|
||||||
|
'gpuservice.instance.event.reason': 'Причина',
|
||||||
|
'gpuservice.instance.event.message': 'Сообщение',
|
||||||
|
'gpuservice.instance.event.source': 'Источник',
|
||||||
|
'gpuservice.instance.event.count': 'Кол-во',
|
||||||
|
'gpuservice.instance.event.lastSeen': 'Последнее событие',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'Отображаются только события за последний час',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'Хранилище',
|
'gpuservice.storage': 'Хранилище',
|
||||||
'gpuservice.storage.add': 'Добавить хранилище',
|
'gpuservice.storage.add': 'Добавить хранилище',
|
||||||
'gpuservice.storage.edit': 'Редактировать хранилище',
|
'gpuservice.storage.edit': 'Редактировать хранилище',
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default {
|
|||||||
'common.button.authorize': 'Rol Yetkilendirme',
|
'common.button.authorize': 'Rol Yetkilendirme',
|
||||||
'common.button.confirm': 'Onayla',
|
'common.button.confirm': 'Onayla',
|
||||||
'common.button.viewlog': 'Günlükleri Görüntüle',
|
'common.button.viewlog': 'Günlükleri Görüntüle',
|
||||||
|
'common.button.viewevent': 'Olayları Görüntüle',
|
||||||
|
'common.button.recreate': 'Yeniden Oluştur',
|
||||||
'common.table.operation': 'İşlemler',
|
'common.table.operation': 'İşlemler',
|
||||||
'common.table.createTime': 'Oluşturulma',
|
'common.table.createTime': 'Oluşturulma',
|
||||||
'common.table.updateTime': 'Güncellenme',
|
'common.table.updateTime': 'Güncellenme',
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export default {
|
|||||||
'gpuservice.template.ports.name': 'Ad',
|
'gpuservice.template.ports.name': 'Ad',
|
||||||
'gpuservice.template.ports.name.max':
|
'gpuservice.template.ports.name.max':
|
||||||
'Bağlantı noktası adı 16 karakterden uzun olamaz.',
|
'Bağlantı noktası adı 16 karakterden uzun olamaz.',
|
||||||
|
'gpuservice.template.ports.name.duplicate':
|
||||||
|
'Bağlantı noktası adları benzersiz olmalıdır.',
|
||||||
'gpuservice.template.env': 'Ortam Değişkenleri',
|
'gpuservice.template.env': 'Ortam Değişkenleri',
|
||||||
'gpuservice.template.env.add': 'Ortam Değişkeni Ekle',
|
'gpuservice.template.env.add': 'Ortam Değişkeni Ekle',
|
||||||
'gpuservice.template.env.invalid': 'Ortam değişkenlerini eksiksiz doldurun.',
|
'gpuservice.template.env.invalid': 'Ortam değişkenlerini eksiksiz doldurun.',
|
||||||
@@ -65,6 +67,17 @@ export default {
|
|||||||
'gpuservice.instance.template.mount': 'Bağlama',
|
'gpuservice.instance.template.mount': 'Bağlama',
|
||||||
'gpuservice.instance.connect': 'Bağlan',
|
'gpuservice.instance.connect': 'Bağlan',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala',
|
'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala',
|
||||||
|
'gpuservice.instance.event.reason': 'Neden',
|
||||||
|
'gpuservice.instance.event.message': 'Mesaj',
|
||||||
|
'gpuservice.instance.event.source': 'Kaynak',
|
||||||
|
'gpuservice.instance.event.count': 'Sayı',
|
||||||
|
'gpuservice.instance.event.lastSeen': 'Son Görülen',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'Yalnızca son bir saatteki olaylar gösterilir',
|
||||||
|
'gpuservice.instance.recreate.confirm.title':
|
||||||
|
'Yeniden oluşturma onaylansın mı',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'Mevcut örnek önce silinecek, ardından mevcut yapılandırmayla yeniden oluşturulacaktır.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'Depolama',
|
'gpuservice.storage': 'Depolama',
|
||||||
'gpuservice.storage.add': 'Depolama Ekle',
|
'gpuservice.storage.add': 'Depolama Ekle',
|
||||||
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export default {
|
|||||||
'common.button.authorize': '角色授权',
|
'common.button.authorize': '角色授权',
|
||||||
'common.button.confirm': '确定',
|
'common.button.confirm': '确定',
|
||||||
'common.button.viewlog': '查看日志',
|
'common.button.viewlog': '查看日志',
|
||||||
|
'common.button.viewevent': '查看事件',
|
||||||
|
'common.button.recreate': '重新创建',
|
||||||
'common.table.operation': '操作',
|
'common.table.operation': '操作',
|
||||||
'common.table.createTime': '创建时间',
|
'common.table.createTime': '创建时间',
|
||||||
'common.table.updateTime': '更新时间',
|
'common.table.updateTime': '更新时间',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default {
|
|||||||
'gpuservice.template.ports.invalid': '请填写完整的端口配置',
|
'gpuservice.template.ports.invalid': '请填写完整的端口配置',
|
||||||
'gpuservice.template.ports.name': '名称',
|
'gpuservice.template.ports.name': '名称',
|
||||||
'gpuservice.template.ports.name.max': '端口名称不能超过 16 个字符',
|
'gpuservice.template.ports.name.max': '端口名称不能超过 16 个字符',
|
||||||
|
'gpuservice.template.ports.name.duplicate': '端口名称不能重复',
|
||||||
'gpuservice.template.env': '环境变量',
|
'gpuservice.template.env': '环境变量',
|
||||||
'gpuservice.template.env.add': '添加环境变量',
|
'gpuservice.template.env.add': '添加环境变量',
|
||||||
'gpuservice.template.env.invalid': '请填写完整的环境变量',
|
'gpuservice.template.env.invalid': '请填写完整的环境变量',
|
||||||
@@ -61,6 +62,15 @@ export default {
|
|||||||
'gpuservice.instance.template.mount': '挂载',
|
'gpuservice.instance.template.mount': '挂载',
|
||||||
'gpuservice.instance.connect': '连接',
|
'gpuservice.instance.connect': '连接',
|
||||||
'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令',
|
'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令',
|
||||||
|
'gpuservice.instance.event.reason': '原因',
|
||||||
|
'gpuservice.instance.event.message': '消息',
|
||||||
|
'gpuservice.instance.event.source': '来源',
|
||||||
|
'gpuservice.instance.event.count': '次数',
|
||||||
|
'gpuservice.instance.event.lastSeen': '最近发生',
|
||||||
|
'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': '确认重新创建',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'系统将先删除当前实例,然后使用当前配置重新创建。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': '存储',
|
'gpuservice.storage': '存储',
|
||||||
'gpuservice.storage.add': '添加存储',
|
'gpuservice.storage.add': '添加存储',
|
||||||
'gpuservice.storage.edit': '编辑存储',
|
'gpuservice.storage.edit': '编辑存储',
|
||||||
|
|||||||
@@ -17,8 +17,27 @@ export const GPU_SERVICE_INSTANCES_API = (params: {
|
|||||||
|
|
||||||
export const GPU_SERVICE_INSTANCES_TYPE_API = (params: {
|
export const GPU_SERVICE_INSTANCES_TYPE_API = (params: {
|
||||||
clusterID?: number;
|
clusterID?: number;
|
||||||
}) =>
|
}) => {
|
||||||
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`;
|
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GPU_SERVICE_INSTANCES_LOG_API = (params: {
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
clusterID?: number;
|
||||||
|
}) => {
|
||||||
|
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/log`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GPU_SERVICE_INSTANCES_EVENTS_API = (params: {
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
clusterID?: number;
|
||||||
|
}) => {
|
||||||
|
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/events`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========== Instances ===========
|
||||||
|
|
||||||
export async function queryGPUServiceInstances(
|
export async function queryGPUServiceInstances(
|
||||||
params: Global.K8sSearchParams & {
|
params: Global.K8sSearchParams & {
|
||||||
@@ -147,10 +166,11 @@ export async function queryGPUServiceInstanceEvents(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return request<InstanceEvents>(
|
return request<InstanceEvents>(
|
||||||
`${GPU_SERVICE_INSTANCES_API({
|
`${GPU_SERVICE_INSTANCES_EVENTS_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
clusterID: params.clusterID
|
clusterID: params.clusterID,
|
||||||
})}/${params.name}/events`,
|
name: params.name
|
||||||
|
})}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: omitPathParams(_.omit(params, ['name'])),
|
params: omitPathParams(_.omit(params, ['name'])),
|
||||||
@@ -171,10 +191,11 @@ export async function queryGPUServiceInstanceLog(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return request<InstanceLog>(
|
return request<InstanceLog>(
|
||||||
`${GPU_SERVICE_INSTANCES_API({
|
`${GPU_SERVICE_INSTANCES_LOG_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
clusterID: params.clusterID
|
clusterID: params.clusterID,
|
||||||
})}/${params.name}/log`,
|
name: params.name
|
||||||
|
})}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: omitPathParams(_.omit(params, ['name'])),
|
params: omitPathParams(_.omit(params, ['name'])),
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
|||||||
<>
|
<>
|
||||||
<span className="dot"></span>
|
<span className="dot"></span>
|
||||||
<span className="meta-item">
|
<span className="meta-item">
|
||||||
<IconFont type="icon-cube" className="meta-icon" />
|
<IconFont type="icon-database" className="meta-icon" />
|
||||||
<Flex align="center" gap={4}>
|
<Flex align="center" gap={4}>
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { createAxiosToken } from '@/hooks/use-chunk-request';
|
||||||
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
AlertBlockInfo,
|
||||||
|
AutoTooltip,
|
||||||
|
ScrollerModal,
|
||||||
|
StatusTag
|
||||||
|
} from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Table } from 'antd';
|
||||||
|
import type { ColumnsType } from 'antd/lib/table';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { queryGPUServiceInstanceEvents } from '../apis';
|
||||||
|
import { InstanceEventItem } from '../config/types';
|
||||||
|
|
||||||
|
type ViewEventsModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
clusterID?: number;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRelative = (text?: string) => (text ? dayjs(text).fromNow() : '-');
|
||||||
|
|
||||||
|
const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
|
||||||
|
Normal: 'success',
|
||||||
|
Warning: 'warning'
|
||||||
|
};
|
||||||
|
|
||||||
|
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { open, onCancel, name, namespace, clusterID } = props || {};
|
||||||
|
const [events, setEvents] = useState<InstanceEventItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const tokenRef = useRef<ReturnType<typeof createAxiosToken> | null>(null);
|
||||||
|
|
||||||
|
const fetchEvents = useCallback(async () => {
|
||||||
|
if (!name || !clusterID || !namespace) return;
|
||||||
|
tokenRef.current?.cancel?.();
|
||||||
|
const source = createAxiosToken();
|
||||||
|
tokenRef.current = source;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await queryGPUServiceInstanceEvents(
|
||||||
|
{ name, namespace, clusterID },
|
||||||
|
{ token: source.token }
|
||||||
|
);
|
||||||
|
setEvents(res?.items ?? []);
|
||||||
|
} catch (e) {
|
||||||
|
// request cancellation or network failure — keep prior items
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [name, namespace, clusterID]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchEvents();
|
||||||
|
} else {
|
||||||
|
tokenRef.current?.cancel?.();
|
||||||
|
setEvents([]);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
tokenRef.current?.cancel?.();
|
||||||
|
};
|
||||||
|
}, [open, fetchEvents]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
tokenRef.current?.cancel?.();
|
||||||
|
onCancel();
|
||||||
|
}, [onCancel]);
|
||||||
|
|
||||||
|
const columns: ColumnsType<InstanceEventItem> = [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'common.table.type' }),
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
width: 110,
|
||||||
|
render: (value: string) => (
|
||||||
|
<StatusTag
|
||||||
|
statusValue={{
|
||||||
|
status: eventTypeStatus[value] ?? 'inactive',
|
||||||
|
text: value || '-'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.instance.event.reason' }),
|
||||||
|
dataIndex: 'reason',
|
||||||
|
key: 'reason',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: { showTitle: false },
|
||||||
|
render: (text: string) => (
|
||||||
|
<AutoTooltip ghost style={{ maxWidth: 180 }}>
|
||||||
|
{text || '-'}
|
||||||
|
</AutoTooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.instance.event.message' }),
|
||||||
|
dataIndex: 'message',
|
||||||
|
key: 'message',
|
||||||
|
ellipsis: { showTitle: false },
|
||||||
|
render: (text: string) => <AutoTooltip ghost>{text || '-'}</AutoTooltip>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.instance.event.source' }),
|
||||||
|
key: 'source',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: { showTitle: false },
|
||||||
|
render: (_text, record) => {
|
||||||
|
const src =
|
||||||
|
record.source?.component ||
|
||||||
|
record.reportingComponent ||
|
||||||
|
record.reportingInstance ||
|
||||||
|
'-';
|
||||||
|
return (
|
||||||
|
<AutoTooltip ghost style={{ maxWidth: 200 }}>
|
||||||
|
{src}
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.instance.event.count' }),
|
||||||
|
dataIndex: 'count',
|
||||||
|
key: 'count',
|
||||||
|
width: 80,
|
||||||
|
render: (value?: number) => value ?? '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.instance.event.lastSeen' }),
|
||||||
|
key: 'lastTimestamp',
|
||||||
|
width: 180,
|
||||||
|
render: (_text, record) =>
|
||||||
|
formatRelative(
|
||||||
|
record.lastTimestamp ||
|
||||||
|
record.series?.lastObservedTime ||
|
||||||
|
record.eventTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollerModal
|
||||||
|
title={
|
||||||
|
<span className="flex flex-center" style={{ gap: 8 }}>
|
||||||
|
<span style={{ fontWeight: 'var(--font-weight-bold)' }}>
|
||||||
|
{intl.formatMessage({ id: 'common.button.viewevent' })}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchEvents}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
zIndex={3000}
|
||||||
|
open={open}
|
||||||
|
centered={true}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
destroyOnHidden={true}
|
||||||
|
closeIcon={true}
|
||||||
|
mask={{
|
||||||
|
closable: false
|
||||||
|
}}
|
||||||
|
keyboard={true}
|
||||||
|
styles={{
|
||||||
|
wrapper: {
|
||||||
|
borderRadius: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
width="1000px"
|
||||||
|
maxContentHeight="100vh"
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<div style={{ marginBlock: '8px 16px' }}>
|
||||||
|
<AlertBlockInfo
|
||||||
|
message={intl.formatMessage({
|
||||||
|
id: 'gpuservice.instance.event.recentHourTip'
|
||||||
|
})}
|
||||||
|
type="warning"
|
||||||
|
contentStyle={{ paddingInline: 0 }}
|
||||||
|
></AlertBlockInfo>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
className={'scroll-table'}
|
||||||
|
tableLayout={'auto'}
|
||||||
|
style={{ width: '100%', minHeight: 400 }}
|
||||||
|
dataSource={events || []}
|
||||||
|
rowKey={(record) =>
|
||||||
|
`${record.metadata?.name}-${record.metadata.namespace}`
|
||||||
|
}
|
||||||
|
loading={{
|
||||||
|
spinning: loading,
|
||||||
|
size: 'middle'
|
||||||
|
}}
|
||||||
|
virtual
|
||||||
|
scroll={{ y: 'calc(100vh - 80px)' }}
|
||||||
|
pagination={false}
|
||||||
|
></Table>
|
||||||
|
</ScrollerModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewEventsModal;
|
||||||
@@ -95,6 +95,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
enableScorllLoad={true}
|
enableScorllLoad={true}
|
||||||
isDownloading={false}
|
isDownloading={false}
|
||||||
params={{
|
params={{
|
||||||
|
watchable: false,
|
||||||
|
watch: false,
|
||||||
|
tailLines: 1000,
|
||||||
follow: true
|
follow: true
|
||||||
}}
|
}}
|
||||||
></LogsViewer>
|
></LogsViewer>
|
||||||
|
|||||||
@@ -52,12 +52,24 @@ export const rowActionList = [
|
|||||||
locale: true,
|
locale: true,
|
||||||
icon: React.createElement(IconFont, { type: 'icon-logs' })
|
icon: React.createElement(IconFont, { type: 'icon-logs' })
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'common.button.viewevent',
|
||||||
|
key: 'viewevent',
|
||||||
|
locale: true,
|
||||||
|
icon: icons.ProfileOutlined
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'common.button.edit',
|
label: 'common.button.edit',
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
locale: true,
|
locale: true,
|
||||||
icon: icons.EditOutlined
|
icon: icons.EditOutlined
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'common.button.recreate',
|
||||||
|
key: 'recreate',
|
||||||
|
locale: true,
|
||||||
|
icon: icons.RetweetOutlined
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'common.button.delete',
|
label: 'common.button.delete',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface InstanceStatus {
|
|||||||
}[];
|
}[];
|
||||||
ports?: {
|
ports?: {
|
||||||
port: number;
|
port: number;
|
||||||
|
name: string;
|
||||||
nodePort?: number;
|
nodePort?: number;
|
||||||
protocol?: InstanceServicePortProtocol;
|
protocol?: InstanceServicePortProtocol;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { validateLabelNameRegxFor63 } from '@/config';
|
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
|
||||||
|
import { PageActionType } from '@/config/types';
|
||||||
import { Input as CInput } from '@gpustack/core-ui';
|
import { Input as CInput } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
|
|
||||||
const Basic = () => {
|
const Basic = ({ action }: { action: PageActionType }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -25,6 +26,7 @@ const Basic = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
|
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -50,7 +51,7 @@ const parseQuantityToNumber = (value?: string): number | null => {
|
|||||||
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
|
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const num = Number(match[1]);
|
const num = Number(match[1]);
|
||||||
return Number.isFinite(num) && num > 0 ? num : null;
|
return Number.isFinite(num) && num > 0 ? _.floor(num, 1) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TABKeysMap = {
|
const TABKeysMap = {
|
||||||
@@ -92,7 +93,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [form] = Form.useForm<InstanceFormValues>();
|
const [form] = Form.useForm<InstanceFormValues>();
|
||||||
const scrollTabsRef = useRef<any>(null);
|
const scrollTabsRef = useRef<any>(null);
|
||||||
const { detailData, fetchData } = useGetSshkey();
|
const { detailData: sshKeyData, fetchData: fetchSSHData } = useGetSshkey();
|
||||||
const { getScrollElementScrollableHeight } = useWrapperContext();
|
const { getScrollElementScrollableHeight } = useWrapperContext();
|
||||||
const {
|
const {
|
||||||
activeKey,
|
activeKey,
|
||||||
@@ -111,9 +112,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
fetchData({});
|
fetchSSHData({});
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open, action]);
|
||||||
|
|
||||||
const segmentOptions = useMemo(
|
const segmentOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -223,7 +224,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}, [action, currentData, form, open, namespace]);
|
}, [action, currentData, form, open, namespace]);
|
||||||
|
|
||||||
const handleFinish = async (values: InstanceFormValues) => {
|
const handleFinish = async (values: InstanceFormValues) => {
|
||||||
await onFinish(values);
|
await onFinish({
|
||||||
|
...values,
|
||||||
|
spec: {
|
||||||
|
...values.spec,
|
||||||
|
sshPublicKey: { name: sshKeyData?.name }
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -272,7 +279,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
accelerator: '1'
|
accelerator: '1'
|
||||||
},
|
},
|
||||||
sshPublicKey: {
|
sshPublicKey: {
|
||||||
name: 'gpustack-organization-ssh-public-key'
|
name: 'gpustack-ssh-public-key'
|
||||||
},
|
},
|
||||||
volume: {
|
volume: {
|
||||||
ephemeral: {
|
ephemeral: {
|
||||||
@@ -286,7 +293,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
enable_ssh: false
|
enable_ssh: false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Basic />
|
<Basic action={action} />
|
||||||
<CollapsePanel
|
<CollapsePanel
|
||||||
activeKey={collapseKeys}
|
activeKey={collapseKeys}
|
||||||
accordion={false}
|
accordion={false}
|
||||||
@@ -309,6 +316,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
children: (
|
children: (
|
||||||
<TemplateBasicForm
|
<TemplateBasicForm
|
||||||
page="instance"
|
page="instance"
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
onceMaxRequest={onceMaxRequest}
|
onceMaxRequest={onceMaxRequest}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -319,7 +327,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
id: 'gpuservice.instance.section.storage'
|
id: 'gpuservice.instance.section.storage'
|
||||||
}),
|
}),
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
children: <StorageVolume />
|
children: (
|
||||||
|
<StorageVolume
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
|
action={action}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||||
|
import { PageAction } from '@/config';
|
||||||
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Flex, Form, Radio } from 'antd';
|
import { Button, Flex, Form, Radio } from 'antd';
|
||||||
@@ -19,7 +20,13 @@ const FieldBlock = styled.div`
|
|||||||
|
|
||||||
const DEFAULT_TEMP_CAPACITY_GB = 50;
|
const DEFAULT_TEMP_CAPACITY_GB = 50;
|
||||||
|
|
||||||
const StorageVolume = () => {
|
const StorageVolume = ({
|
||||||
|
disabled,
|
||||||
|
action
|
||||||
|
}: {
|
||||||
|
disabled?: boolean;
|
||||||
|
action: PageActionType;
|
||||||
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { fetchData: createStorage } = useCreateStorage();
|
const { fetchData: createStorage } = useCreateStorage();
|
||||||
const { detailData: storageData, fetchData: fetchStorage } =
|
const { detailData: storageData, fetchData: fetchStorage } =
|
||||||
@@ -82,6 +89,7 @@ const StorageVolume = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
|
disabled={disabled}
|
||||||
style={{ marginBottom: 12 }}
|
style={{ marginBottom: 12 }}
|
||||||
value={storageMode}
|
value={storageMode}
|
||||||
onChange={(e) => handleModeChange(e.target.value)}
|
onChange={(e) => handleModeChange(e.target.value)}
|
||||||
@@ -98,6 +106,7 @@ const StorageVolume = () => {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{action === PageAction.CREATE && (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -106,6 +115,7 @@ const StorageVolume = () => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{storageMode === StorageModeValueMap.Existing && (
|
{storageMode === StorageModeValueMap.Existing && (
|
||||||
@@ -121,6 +131,7 @@ const StorageVolume = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storage.persistentVolume'
|
id: 'gpuservice.storage.persistentVolume'
|
||||||
})}
|
})}
|
||||||
@@ -149,6 +160,7 @@ const StorageVolume = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CInputNumber
|
<CInputNumber
|
||||||
|
disabled={disabled}
|
||||||
min={1}
|
min={1}
|
||||||
precision={0}
|
precision={0}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
|
|||||||
@@ -9,13 +9,27 @@ import { useIntl } from '@umijs/max';
|
|||||||
import { Button, Flex } from 'antd';
|
import { Button, Flex } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/lib/table';
|
import type { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import _ from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
|
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
type ConnectEntry =
|
type ConnectEntry =
|
||||||
| { type: 'ssh'; key: string; command: string }
|
| {
|
||||||
| { type: 'http'; key: string; port: number; url: string };
|
type: 'ssh';
|
||||||
|
key: string;
|
||||||
|
command: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'http';
|
||||||
|
key: string;
|
||||||
|
port: number;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
};
|
||||||
|
|
||||||
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||||
const ip = record.status?.hostIPs?.[0]?.ip;
|
const ip = record.status?.hostIPs?.[0]?.ip;
|
||||||
@@ -33,13 +47,17 @@ const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
|||||||
return isSsh
|
return isSsh
|
||||||
? {
|
? {
|
||||||
type: 'ssh',
|
type: 'ssh',
|
||||||
|
name: 'SSH',
|
||||||
key: `ssh-${p.nodePort}`,
|
key: `ssh-${p.nodePort}`,
|
||||||
|
protocol: _.toUpper(p.protocol),
|
||||||
command: `ssh root@${ip} -p ${p.nodePort}`
|
command: `ssh root@${ip} -p ${p.nodePort}`
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'http',
|
type: 'http',
|
||||||
|
name: p.name || 'HTTP',
|
||||||
key: `http-${p.nodePort}`,
|
key: `http-${p.nodePort}`,
|
||||||
port: p.port,
|
port: p.port,
|
||||||
|
protocol: _.toUpper(p.protocol),
|
||||||
url: `http://${ip}:${p.nodePort}`
|
url: `http://${ip}:${p.nodePort}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -116,7 +134,7 @@ const useInstancesColumns = ({
|
|||||||
className="text-tertiary"
|
className="text-tertiary"
|
||||||
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
HTTP
|
{entry.name || entry.protocol}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
|
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
|
const useViewEvents = () => {
|
||||||
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
|
const clusterID = currentCluster?.id;
|
||||||
|
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||||
|
|
||||||
|
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
clusterID?: number;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
name: '',
|
||||||
|
namespace: '',
|
||||||
|
clusterID: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const openModal = (row?: ListItem) => {
|
||||||
|
const name = row?.metadata?.name || '';
|
||||||
|
const rowNamespace = row?.metadata?.namespace || namespace;
|
||||||
|
setOpenModalStatus({
|
||||||
|
open: true,
|
||||||
|
name,
|
||||||
|
namespace: rowNamespace,
|
||||||
|
clusterID
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setOpenModalStatus({
|
||||||
|
open: false,
|
||||||
|
name: '',
|
||||||
|
namespace: '',
|
||||||
|
clusterID: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
openViewEventsModalStatus: openModalStatus,
|
||||||
|
openViewEventsModal: openModal,
|
||||||
|
closeViewEventsModal: closeModal
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useViewEvents;
|
||||||
@@ -2,7 +2,7 @@ import { currentClusterAtom } from '@/atoms/gpuservice';
|
|||||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { GPU_SERVICE_INSTANCES_API } from '../apis';
|
import { GPU_SERVICE_INSTANCES_LOG_API } from '../apis';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
const useViewLogs = () => {
|
const useViewLogs = () => {
|
||||||
@@ -29,10 +29,11 @@ const useViewLogs = () => {
|
|||||||
open: true,
|
open: true,
|
||||||
url:
|
url:
|
||||||
name && clusterID
|
name && clusterID
|
||||||
? `${GPU_SERVICE_INSTANCES_API({
|
? `${GPU_SERVICE_INSTANCES_LOG_API({
|
||||||
namespace: rowNamespace,
|
namespace: rowNamespace,
|
||||||
clusterID
|
clusterID,
|
||||||
})}/${name}/log`
|
name
|
||||||
|
})}`
|
||||||
: '',
|
: '',
|
||||||
tail: 1000,
|
tail: 1000,
|
||||||
status: row?.status?.phase || undefined
|
status: row?.status?.phase || undefined
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
|
import { ConfigProvider, Divider, Flex, message, Modal, Table } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
@@ -26,15 +26,18 @@ import {
|
|||||||
queryGPUServiceInstances
|
queryGPUServiceInstances
|
||||||
} from './apis';
|
} from './apis';
|
||||||
import AddModal from './components/add-modal';
|
import AddModal from './components/add-modal';
|
||||||
|
import ViewEventsModal from './components/view-events-modal';
|
||||||
import ViewLogsModal from './components/view-logs-modal';
|
import ViewLogsModal from './components/view-logs-modal';
|
||||||
import { FormData, ListItem } from './config/types';
|
import { FormData, ListItem } from './config/types';
|
||||||
import useInstancesColumns from './hooks/use-instances-columns';
|
import useInstancesColumns from './hooks/use-instances-columns';
|
||||||
|
import useViewEvents from './hooks/use-view-events';
|
||||||
import useViewLogs from './hooks/use-view-logs';
|
import useViewLogs from './hooks/use-view-logs';
|
||||||
import useCreateInstance from './services/use-create-instance';
|
import useCreateInstance from './services/use-create-instance';
|
||||||
import useUpdateInstance from './services/use-update-instance';
|
import useUpdateInstance from './services/use-update-instance';
|
||||||
|
|
||||||
const GPUService: React.FC = () => {
|
const GPUService: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
// In admin "All" view there's no Org context, so the helper falls
|
// In admin "All" view there's no Org context, so the helper falls
|
||||||
@@ -67,10 +70,10 @@ const GPUService: React.FC = () => {
|
|||||||
{ ...params, namespace, clusterID: effectiveClusterID },
|
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const total = res.items?.length ?? 0;
|
const total = res?.items?.length ?? 0;
|
||||||
const perPage = params.perPage || 10;
|
const perPage = params.perPage || 10;
|
||||||
return {
|
return {
|
||||||
items: res.items ?? [],
|
items: res?.items ?? [],
|
||||||
pagination: {
|
pagination: {
|
||||||
total,
|
total,
|
||||||
totalPage: Math.ceil(total / perPage),
|
totalPage: Math.ceil(total / perPage),
|
||||||
@@ -109,6 +112,11 @@ const GPUService: React.FC = () => {
|
|||||||
const { fetchData: updateInstance } = useUpdateInstance();
|
const { fetchData: updateInstance } = useUpdateInstance();
|
||||||
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
|
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
|
||||||
useViewLogs();
|
useViewLogs();
|
||||||
|
const {
|
||||||
|
openViewEventsModal,
|
||||||
|
closeViewEventsModal,
|
||||||
|
openViewEventsModalStatus
|
||||||
|
} = useViewEvents();
|
||||||
const {
|
const {
|
||||||
fetchClusterList,
|
fetchClusterList,
|
||||||
cancelRequest: cancelClusterRequest,
|
cancelRequest: cancelClusterRequest,
|
||||||
@@ -210,6 +218,36 @@ const GPUService: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRecreate = useMemoizedFn((row: ListItem) => {
|
||||||
|
modalRef.current?.show({
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'gpuservice.instance.recreate.confirm.title'
|
||||||
|
}),
|
||||||
|
content: 'gpuservice.instance.recreate.confirm.content',
|
||||||
|
okText: 'common.button.recreate',
|
||||||
|
operation: 'gpuservice.instance.recreate.confirm.content',
|
||||||
|
name: row.metadata?.name,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteInstance(row.metadata?.name as any);
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
await createInstance({
|
||||||
|
data: {
|
||||||
|
metadata: { name: row.metadata.name, namespace },
|
||||||
|
spec: row.spec
|
||||||
|
} as FormData
|
||||||
|
});
|
||||||
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
|
handleSearch();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(intl.formatMessage({ id: 'common.message.fail' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
|
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
|
||||||
if (val === 'edit') {
|
if (val === 'edit') {
|
||||||
handleEditInstance(row);
|
handleEditInstance(row);
|
||||||
@@ -219,8 +257,12 @@ const GPUService: React.FC = () => {
|
|||||||
name: row.metadata?.name,
|
name: row.metadata?.name,
|
||||||
id: row.metadata?.name as any
|
id: row.metadata?.name as any
|
||||||
});
|
});
|
||||||
|
} else if (val === 'recreate') {
|
||||||
|
handleRecreate(row);
|
||||||
} else if (val === 'viewlog') {
|
} else if (val === 'viewlog') {
|
||||||
openViewLogsModal(row);
|
openViewLogsModal(row);
|
||||||
|
} else if (val === 'viewevent') {
|
||||||
|
openViewEventsModal(row);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,15 +358,7 @@ const GPUService: React.FC = () => {
|
|||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
rowKey={(record) => record.metadata.name}
|
rowKey={(record) => record.metadata.name}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={false}
|
||||||
size: 'middle',
|
|
||||||
showSizeChanger: true,
|
|
||||||
pageSize: queryParams.perPage,
|
|
||||||
current: queryParams.page,
|
|
||||||
total: dataSource.total,
|
|
||||||
hideOnSinglePage: queryParams.perPage === 10,
|
|
||||||
onChange: handlePageChange
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
<AddModal
|
<AddModal
|
||||||
@@ -341,7 +375,15 @@ const GPUService: React.FC = () => {
|
|||||||
tail={openViewLogsModalStatus.tail}
|
tail={openViewLogsModalStatus.tail}
|
||||||
onCancel={closeViewLogsModal}
|
onCancel={closeViewLogsModal}
|
||||||
/>
|
/>
|
||||||
|
<ViewEventsModal
|
||||||
|
open={openViewEventsModalStatus.open}
|
||||||
|
name={openViewEventsModalStatus.name}
|
||||||
|
namespace={openViewEventsModalStatus.namespace}
|
||||||
|
clusterID={openViewEventsModalStatus.clusterID}
|
||||||
|
onCancel={closeViewEventsModal}
|
||||||
|
/>
|
||||||
<DeleteModal ref={modalRef} />
|
<DeleteModal ref={modalRef} />
|
||||||
|
{modalContextHolder}
|
||||||
</PageContainerInner>
|
</PageContainerInner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -11,8 +11,8 @@ interface QueryInstanceLogParams extends InstanceLogQueryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useQueryInstanceLog() {
|
export default function useQueryInstanceLog() {
|
||||||
const namespace = getCurrentOrganizationId();
|
|
||||||
const currentCluster = useAtomValue(currentClusterAtom);
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
|
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
const fetchDetail = useCallback(
|
const fetchDetail = useCallback(
|
||||||
|
|||||||
@@ -98,12 +98,12 @@ export const ReClaimPolicyOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const rowActionList = [
|
export const rowActionList = [
|
||||||
{
|
// {
|
||||||
label: 'common.button.edit',
|
// label: 'common.button.edit',
|
||||||
key: 'edit',
|
// key: 'edit',
|
||||||
locale: true,
|
// locale: true,
|
||||||
icon: icons.EditOutlined
|
// icon: icons.EditOutlined
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: 'common.button.delete',
|
label: 'common.button.delete',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ListItem {
|
|||||||
accessMode: AccessModeType;
|
accessMode: AccessModeType;
|
||||||
};
|
};
|
||||||
status: {
|
status: {
|
||||||
|
phaseMessage?: string;
|
||||||
phase: 'Available' | 'Unavailable' | 'Pending';
|
phase: 'Available' | 'Unavailable' | 'Pending';
|
||||||
volume: {
|
volume: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { validateLabelNameRegxFor63 } from '@/config';
|
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
|
||||||
import {
|
import {
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
@@ -11,7 +11,7 @@ import { useEffect } from 'react';
|
|||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
import useQueryStorageClass from '../services/use-query-storage-class';
|
import useQueryStorageClass from '../services/use-query-storage-class';
|
||||||
|
|
||||||
const Basic = ({ open }: { open: boolean }) => {
|
const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const { storageClassList, fetchData, loading } = useQueryStorageClass();
|
const { storageClassList, fetchData, loading } = useQueryStorageClass();
|
||||||
@@ -38,6 +38,7 @@ const Basic = ({ open }: { open: boolean }) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -54,6 +55,7 @@ const Basic = ({ open }: { open: boolean }) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
|
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
|
||||||
required
|
required
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -76,6 +78,7 @@ const Basic = ({ open }: { open: boolean }) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.storage.capacity' })}
|
label={intl.formatMessage({ id: 'gpuservice.storage.capacity' })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
|||||||
preserve={false}
|
preserve={false}
|
||||||
initialValues={{}}
|
initialValues={{}}
|
||||||
>
|
>
|
||||||
<Basic open={open} />
|
<Basic open={open} action={action} />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ const useStorageColumns = ({
|
|||||||
dataIndex: ['status', 'phase'],
|
dataIndex: ['status', 'phase'],
|
||||||
key: 'status',
|
key: 'status',
|
||||||
sorter: false,
|
sorter: false,
|
||||||
render: (value: string) => (
|
render: (value: string, record: ListItem) => (
|
||||||
<StatusTag
|
<StatusTag
|
||||||
statusValue={{
|
statusValue={{
|
||||||
status: status[value],
|
status: status[value],
|
||||||
text: StoragePhaseLabelMap[value] || value
|
text: StoragePhaseLabelMap[value] || value,
|
||||||
|
message: record.status?.phaseMessage || ''
|
||||||
}}
|
}}
|
||||||
></StatusTag>
|
></StatusTag>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
{ ...params, namespace, clusterID: effectiveClusterID },
|
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const total = res.items?.length ?? 0;
|
const total = res?.items?.length ?? 0;
|
||||||
const perPage = params.perPage || 10;
|
const perPage = params.perPage || 10;
|
||||||
return {
|
return {
|
||||||
items: res.items ?? [],
|
items: res?.items ?? [],
|
||||||
pagination: {
|
pagination: {
|
||||||
total,
|
total,
|
||||||
totalPage: Math.ceil(total / perPage),
|
totalPage: Math.ceil(total / perPage),
|
||||||
@@ -311,15 +311,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
rowKey={(record) => record.metadata?.name as any}
|
rowKey={(record) => record.metadata?.name as any}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={false}
|
||||||
size: 'middle',
|
|
||||||
showSizeChanger: true,
|
|
||||||
pageSize: queryParams.perPage,
|
|
||||||
current: queryParams.page,
|
|
||||||
total: dataSource.total,
|
|
||||||
hideOnSinglePage: queryParams.perPage === 10,
|
|
||||||
onChange: handlePageChange
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</PageContainerInner>
|
</PageContainerInner>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { validateLabelNameRegxFor63 } from '@/config';
|
||||||
import {
|
import {
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
@@ -26,10 +27,15 @@ export interface BasicResourceMax {
|
|||||||
|
|
||||||
interface BasicProps {
|
interface BasicProps {
|
||||||
page?: 'template' | 'instance';
|
page?: 'template' | 'instance';
|
||||||
|
disabled?: boolean;
|
||||||
onceMaxRequest?: BasicResourceMax;
|
onceMaxRequest?: BasicResourceMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
const Basic: React.FC<BasicProps> = ({
|
||||||
|
page = 'template',
|
||||||
|
onceMaxRequest,
|
||||||
|
disabled
|
||||||
|
}) => {
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -68,10 +74,17 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: getRuleMessage('input', 'common.table.name')
|
message: getRuleMessage('input', 'common.table.name')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: validateLabelNameRegxFor63,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'gpuservice.form.rule.name'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -105,6 +118,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({ id: 'resources.table.vendor' })}
|
label={intl.formatMessage({ id: 'resources.table.vendor' })}
|
||||||
required
|
required
|
||||||
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
|
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
|
||||||
@@ -123,12 +137,14 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.template.image' })}
|
label={intl.formatMessage({ id: 'gpuservice.template.image' })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 'imagePullPolicy']}>
|
<Form.Item<FormData> name={['spec', 'imagePullPolicy']}>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.template.imagePullPolicy'
|
id: 'gpuservice.template.imagePullPolicy'
|
||||||
})}
|
})}
|
||||||
@@ -144,6 +160,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
getValueProps={(value) => ({ value: stringifyCommand(value) })}
|
getValueProps={(value) => ({ value: stringifyCommand(value) })}
|
||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
|
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: 'gpuservice.template.command.placeholder'
|
id: 'gpuservice.template.command.placeholder'
|
||||||
@@ -164,6 +181,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: 'clusters.volume.mountPath.format'
|
id: 'clusters.volume.mountPath.format'
|
||||||
})}
|
})}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +199,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
onceMaxRequest?.localStorage
|
onceMaxRequest?.localStorage
|
||||||
)}
|
)}
|
||||||
max={onceMaxRequest?.localStorage ?? undefined}
|
max={onceMaxRequest?.localStorage ?? undefined}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,6 +214,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
<InputNumber
|
<InputNumber
|
||||||
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
|
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
|
||||||
max={onceMaxRequest?.cpu ?? undefined}
|
max={onceMaxRequest?.cpu ?? undefined}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,13 +232,14 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
|
|||||||
onceMaxRequest?.memory
|
onceMaxRequest?.memory
|
||||||
)}
|
)}
|
||||||
max={onceMaxRequest?.memory ?? undefined}
|
max={onceMaxRequest?.memory ?? undefined}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Ports />
|
<Ports disabled={disabled} />
|
||||||
<Env />
|
<Env disabled={disabled} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,17 @@ interface EnvItemProps {
|
|||||||
onChange: (item: EnvItemType) => void;
|
onChange: (item: EnvItemType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
|
const EnvItem: React.FC<EnvItemProps & { disabled?: boolean }> = ({
|
||||||
|
item,
|
||||||
|
onChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
value={item.name}
|
value={item.name}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: 'gpuservice.template.env.name'
|
id: 'gpuservice.template.env.name'
|
||||||
@@ -30,6 +35,7 @@ const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: 'gpuservice.template.env.value'
|
id: 'gpuservice.template.env.value'
|
||||||
@@ -47,7 +53,7 @@ const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Env: React.FC = () => {
|
const Env: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const env = Form.useWatch(['spec', 'env'], form) || [];
|
const env = Form.useWatch(['spec', 'env'], form) || [];
|
||||||
@@ -103,6 +109,7 @@ const Env: React.FC = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<MetadataList
|
<MetadataList
|
||||||
|
disabled={disabled}
|
||||||
dataList={env}
|
dataList={env}
|
||||||
btnText={intl.formatMessage({ id: 'gpuservice.template.env.add' })}
|
btnText={intl.formatMessage({ id: 'gpuservice.template.env.add' })}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.template.env' })}
|
label={intl.formatMessage({ id: 'gpuservice.template.env' })}
|
||||||
@@ -111,6 +118,7 @@ const Env: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{(item, index) => (
|
{(item, index) => (
|
||||||
<EnvItem
|
<EnvItem
|
||||||
|
disabled={disabled}
|
||||||
key={index}
|
key={index}
|
||||||
index={index}
|
index={index}
|
||||||
item={item}
|
item={item}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import {
|
|||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FormData, PortItem as PortItemType } from '../config/types';
|
import { FormData, PortItem as PortItemType } from '../config/types';
|
||||||
|
|
||||||
type PortProtocol = PortItemType['protocol'];
|
type PortProtocol = PortItemType['protocol'];
|
||||||
|
|
||||||
const PORT_NAME_MAX = 16;
|
const PORT_NAME_MAX = 16;
|
||||||
|
const PORT_MIN = 1;
|
||||||
|
const PORT_MAX = 65535;
|
||||||
|
|
||||||
const protocolOptions = [
|
const protocolOptions = [
|
||||||
{
|
{
|
||||||
@@ -25,9 +28,9 @@ const protocolOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const wellKnownTcpPortNames: Record<number, string> = {
|
const wellKnownTcpPortNames: Record<number, string> = {
|
||||||
22: 'SSH',
|
22: 'ssh',
|
||||||
80: 'HTTP',
|
80: 'http',
|
||||||
443: 'HTTPS'
|
443: 'https'
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAutoFilledName = (
|
const getAutoFilledName = (
|
||||||
@@ -57,6 +60,7 @@ const getAutoFilledName = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface PortItemProps {
|
interface PortItemProps {
|
||||||
|
disabled?: boolean;
|
||||||
item: PortItemType;
|
item: PortItemType;
|
||||||
index: number;
|
index: number;
|
||||||
onChange: (item: PortItemType) => void;
|
onChange: (item: PortItemType) => void;
|
||||||
@@ -68,10 +72,11 @@ interface PortNameInputProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PortNameInput: React.FC<PortNameInputProps> = ({
|
const PortNameInput: React.FC<PortNameInputProps & { disabled?: boolean }> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder
|
placeholder,
|
||||||
|
disabled = false
|
||||||
}) => {
|
}) => {
|
||||||
const [localValue, setLocalValue] = useState<string>(value ?? '');
|
const [localValue, setLocalValue] = useState<string>(value ?? '');
|
||||||
const isComposingRef = useRef(false);
|
const isComposingRef = useRef(false);
|
||||||
@@ -85,6 +90,7 @@ const PortNameInput: React.FC<PortNameInputProps> = ({
|
|||||||
return (
|
return (
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
showCount
|
showCount
|
||||||
|
disabled={disabled}
|
||||||
maxLength={PORT_NAME_MAX}
|
maxLength={PORT_NAME_MAX}
|
||||||
value={localValue}
|
value={localValue}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -109,12 +115,13 @@ const PortNameInput: React.FC<PortNameInputProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
|
const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, index, disabled = false, onChange }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
||||||
<div style={{ width: 120 }}>
|
<div style={{ width: 120 }}>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
|
disabled={disabled}
|
||||||
value={item.protocol}
|
value={item.protocol}
|
||||||
options={protocolOptions}
|
options={protocolOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -136,8 +143,9 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ width: 120 }}>
|
<div style={{ width: 120 }}>
|
||||||
<CInputNumber
|
<CInputNumber
|
||||||
min={1}
|
min={PORT_MIN}
|
||||||
max={65535}
|
max={PORT_MAX}
|
||||||
|
disabled={disabled}
|
||||||
precision={0}
|
precision={0}
|
||||||
value={item.port}
|
value={item.port}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -160,6 +168,7 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<PortNameInput
|
<PortNameInput
|
||||||
|
disabled={disabled}
|
||||||
value={item.name}
|
value={item.name}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: 'gpuservice.template.ports.name'
|
id: 'gpuservice.template.ports.name'
|
||||||
@@ -176,13 +185,15 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Ports: React.FC = () => {
|
const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const ports = Form.useWatch(['spec', 'ports'], form) || [];
|
const ports = Form.useWatch(['spec', 'ports'], form) || [];
|
||||||
|
|
||||||
const updatePorts = (list: PortItemType[]) => {
|
const updatePorts = (list: PortItemType[]) => {
|
||||||
form.setFieldValue(['spec', 'ports'], list);
|
form.setFieldValue(['spec', 'ports'], list);
|
||||||
|
// setFieldValue doesn't run validators — re-run them so users see errors live.
|
||||||
|
form.validateFields([['spec', 'ports']]).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@@ -213,27 +224,31 @@ const Ports: React.FC = () => {
|
|||||||
name={['spec', 'ports']}
|
name={['spec', 'ports']}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
validator: async (_, value) => {
|
validator: async (_rule, value) => {
|
||||||
if (!value?.length) {
|
if (!value?.length) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
const hasInvalidPort = value.some(
|
|
||||||
(item: PortItemType) => !item.protocol || !item.port
|
const items = value as PortItemType[];
|
||||||
);
|
|
||||||
if (hasInvalidPort) {
|
if (_.some(items, (item) => !item.protocol || !item.port)) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error(
|
new Error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(
|
||||||
id: 'gpuservice.template.ports.invalid'
|
{ id: 'common.validate.value' },
|
||||||
|
{
|
||||||
|
name: intl.formatMessage({
|
||||||
|
id: 'gpuservice.template.ports'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const hasInvalidName = value.some(
|
|
||||||
(item: PortItemType) =>
|
const namedItems = items.filter((item) => !!item.name);
|
||||||
item.name && item.name.length > PORT_NAME_MAX
|
|
||||||
);
|
if (_.some(namedItems, (item) => item.name!.length > PORT_NAME_MAX)) {
|
||||||
if (hasInvalidName) {
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error(
|
new Error(
|
||||||
intl.formatMessage({
|
intl.formatMessage({
|
||||||
@@ -242,12 +257,25 @@ const Ports: React.FC = () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const names = namedItems.map((item) => item.name);
|
||||||
|
if (_.uniq(names).length !== names.length) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'gpuservice.template.ports.name.duplicate'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<MetadataList
|
<MetadataList
|
||||||
|
disabled={disabled}
|
||||||
dataList={ports}
|
dataList={ports}
|
||||||
btnText={intl.formatMessage({ id: 'gpuservice.template.ports.add' })}
|
btnText={intl.formatMessage({ id: 'gpuservice.template.ports.add' })}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.template.ports' })}
|
label={intl.formatMessage({ id: 'gpuservice.template.ports' })}
|
||||||
@@ -256,6 +284,7 @@ const Ports: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{(item, index) => (
|
{(item, index) => (
|
||||||
<PortItem
|
<PortItem
|
||||||
|
disabled={disabled}
|
||||||
key={index}
|
key={index}
|
||||||
index={index}
|
index={index}
|
||||||
item={item}
|
item={item}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { useIntl } from '@umijs/max';
|
|||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { GPUsConfigs } from '../../resources/config/gpu-driver';
|
|
||||||
import PageBox from '../../_components/page-box';
|
import PageBox from '../../_components/page-box';
|
||||||
|
import { GPUsConfigs } from '../../resources/config/gpu-driver';
|
||||||
import {
|
import {
|
||||||
createGPUServiceTemplate,
|
createGPUServiceTemplate,
|
||||||
deleteGPUServiceTemplate,
|
deleteGPUServiceTemplate,
|
||||||
@@ -55,7 +55,7 @@ const GPUServiceTemplates: React.FC = () => {
|
|||||||
() => [
|
() => [
|
||||||
...Object.values(GPUsConfigs).map((item) => ({
|
...Object.values(GPUsConfigs).map((item) => ({
|
||||||
label: item.label,
|
label: item.label,
|
||||||
value: item.value
|
value: item.gpuVendor as string
|
||||||
})),
|
})),
|
||||||
{ label: 'CPU', value: 'cpu' }
|
{ label: 'CPU', value: 'cpu' }
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user