diff --git a/packages/backend.ai-ui/src/components/BAISelect.tsx b/packages/backend.ai-ui/src/components/BAISelect.tsx index b6ecec7b26..e73358efed 100644 --- a/packages/backend.ai-ui/src/components/BAISelect.tsx +++ b/packages/backend.ai-ui/src/components/BAISelect.tsx @@ -43,6 +43,24 @@ const useStyles = createStyles(({ css, token }) => ({ color: ${token.colorBgBase}; } } + + /* In ghost mode the border-color is hard-overridden with !important, + which would otherwise swallow antd's status="error" red border. + Restore the error treatment with a higher-specificity rule so a + ghost select (e.g. the header ProjectSelect) can still surface an + error state. */ + &.ant-select.ant-select-status-error { + border-color: ${token.colorError} !important; + + .ant-select-suffix { + color: ${token.colorError}; + } + + &:hover .ant-select-suffix, + &:active .ant-select-suffix { + color: ${token.colorError}; + } + } `, customStyle: css` /* Hide selected value content (except search input) when the user is typing a search query. diff --git a/react/src/components/ProjectSelect.tsx b/react/src/components/ProjectSelect.tsx index bc9cfe3eea..1158eb6a70 100644 --- a/react/src/components/ProjectSelect.tsx +++ b/react/src/components/ProjectSelect.tsx @@ -7,6 +7,7 @@ import { useSuspendedBackendaiClient } from '../hooks'; import { useCurrentUserInfo, useCurrentUserRole } from '../hooks/backendai'; import useControllableState_deprecated from '../hooks/useControllableState'; import { useCurrentUserProjectRoles } from '../hooks/useCurrentUserProjectRoles'; +import { InfoCircleOutlined } from '@ant-design/icons'; import { theme, Tooltip } from 'antd'; import { BAIFlex, BAISelect, BAISelectProps } from 'backend.ai-ui'; import * as _ from 'lodash-es'; @@ -153,6 +154,13 @@ const ProjectSelect: React.FC = ({ }, ); + const showNoProjectError = + !accessibleProjects?.length && + !selectProps.disabled && + !selectProps.loading; + + const noAccessibleProjectsMessage = t('projectSelect.NoAccessibleProjects'); + return ( { @@ -169,6 +177,24 @@ const ProjectSelect: React.FC = ({ options={ _.size(groupOptions) > 1 ? groupOptions : groupOptions[0]?.options } + status={showNoProjectError ? 'error' : selectProps.status} + // Surface the empty-state reason on the focusable control itself, + // not only on the non-focusable suffix icon. `tooltip` wraps the + // whole BAISelect in an antd Tooltip (hover over the entire control, + // not just the tiny icon), and `aria-label` gives keyboard / + // screen-reader users a persistent accessible description that does + // not depend on the tooltip being open. + tooltip={ + showNoProjectError ? noAccessibleProjectsMessage : selectProps.tooltip + } + aria-label={ + showNoProjectError + ? noAccessibleProjectsMessage + : selectProps['aria-label'] + } + suffixIcon={ + showNoProjectError ? : selectProps.suffixIcon + } /> ); }; diff --git a/resources/i18n/de.json b/resources/i18n/de.json index 4d31941cb7..383ba8ea05 100644 --- a/resources/i18n/de.json +++ b/resources/i18n/de.json @@ -2113,6 +2113,7 @@ "RoleMember": "Mitglied" }, "projectSelect": { + "NoAccessibleProjects": "Keine zugänglichen Projekte.", "ProjectAdminBadge": "Projektadministrator" }, "prometheusQueryPreset": { diff --git a/resources/i18n/el.json b/resources/i18n/el.json index 3fdd33dd50..3cff62e1c1 100644 --- a/resources/i18n/el.json +++ b/resources/i18n/el.json @@ -2111,6 +2111,7 @@ "RoleMember": "Μέλος" }, "projectSelect": { + "NoAccessibleProjects": "Δεν υπάρχουν προσβάσιμα έργα.", "ProjectAdminBadge": "Διαχειριστής έργου" }, "prometheusQueryPreset": { diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 592dc9d93c..70607af74a 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -2130,6 +2130,7 @@ "RoleMember": "Member" }, "projectSelect": { + "NoAccessibleProjects": "No accessible projects.", "ProjectAdminBadge": "Project Admin" }, "prometheusQueryPreset": { diff --git a/resources/i18n/es.json b/resources/i18n/es.json index fabaf4a1f9..1695446697 100644 --- a/resources/i18n/es.json +++ b/resources/i18n/es.json @@ -2111,6 +2111,7 @@ "RoleMember": "Miembro" }, "projectSelect": { + "NoAccessibleProjects": "No hay proyectos accesibles.", "ProjectAdminBadge": "Administrador del proyecto" }, "prometheusQueryPreset": { diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json index d229b3f62a..1cf7bae01d 100644 --- a/resources/i18n/fi.json +++ b/resources/i18n/fi.json @@ -2111,6 +2111,7 @@ "RoleMember": "Jäsen" }, "projectSelect": { + "NoAccessibleProjects": "Ei saatavilla olevia projekteja.", "ProjectAdminBadge": "Projektin ylläpitäjä" }, "prometheusQueryPreset": { diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index ea4162f099..2a6e459330 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -2113,6 +2113,7 @@ "RoleMember": "Membre" }, "projectSelect": { + "NoAccessibleProjects": "Aucun projet accessible.", "ProjectAdminBadge": "Administrateur de projet" }, "prometheusQueryPreset": { diff --git a/resources/i18n/id.json b/resources/i18n/id.json index 891951a480..cb78f2ddd8 100644 --- a/resources/i18n/id.json +++ b/resources/i18n/id.json @@ -2114,6 +2114,7 @@ "RoleMember": "Anggota" }, "projectSelect": { + "NoAccessibleProjects": "Tidak ada proyek yang dapat diakses.", "ProjectAdminBadge": "Admin Proyek" }, "prometheusQueryPreset": { diff --git a/resources/i18n/it.json b/resources/i18n/it.json index e290140d1e..8f43ffac7e 100644 --- a/resources/i18n/it.json +++ b/resources/i18n/it.json @@ -2111,6 +2111,7 @@ "RoleMember": "Membro" }, "projectSelect": { + "NoAccessibleProjects": "Nessun progetto accessibile.", "ProjectAdminBadge": "Amministratore del progetto" }, "prometheusQueryPreset": { diff --git a/resources/i18n/ja.json b/resources/i18n/ja.json index 47432889d4..de28919a69 100644 --- a/resources/i18n/ja.json +++ b/resources/i18n/ja.json @@ -2113,6 +2113,7 @@ "RoleMember": "メンバー" }, "projectSelect": { + "NoAccessibleProjects": "アクセス可能なプロジェクトがありません。", "ProjectAdminBadge": "プロジェクト管理者" }, "prometheusQueryPreset": { diff --git a/resources/i18n/ko.json b/resources/i18n/ko.json index b971cd5b88..dfaef7c076 100644 --- a/resources/i18n/ko.json +++ b/resources/i18n/ko.json @@ -2114,6 +2114,7 @@ "RoleMember": "구성원" }, "projectSelect": { + "NoAccessibleProjects": "선택 가능한 프로젝트가 없습니다.", "ProjectAdminBadge": "프로젝트 관리자" }, "prometheusQueryPreset": { diff --git a/resources/i18n/mn.json b/resources/i18n/mn.json index aeff3d1500..8aa91e3735 100644 --- a/resources/i18n/mn.json +++ b/resources/i18n/mn.json @@ -2112,6 +2112,7 @@ "RoleMember": "Гишүүн" }, "projectSelect": { + "NoAccessibleProjects": "Боломжтой төсөл байхгүй байна.", "ProjectAdminBadge": "Төслийн администратор" }, "prometheusQueryPreset": { diff --git a/resources/i18n/ms.json b/resources/i18n/ms.json index 7bb247ac26..310fe06cf5 100644 --- a/resources/i18n/ms.json +++ b/resources/i18n/ms.json @@ -2111,6 +2111,7 @@ "RoleMember": "Ahli" }, "projectSelect": { + "NoAccessibleProjects": "Tiada projek yang boleh diakses.", "ProjectAdminBadge": "Pentadbir Projek" }, "prometheusQueryPreset": { diff --git a/resources/i18n/pl.json b/resources/i18n/pl.json index 67755b458d..a005614d40 100644 --- a/resources/i18n/pl.json +++ b/resources/i18n/pl.json @@ -2113,6 +2113,7 @@ "RoleMember": "Członek" }, "projectSelect": { + "NoAccessibleProjects": "Brak dostępnych projektów.", "ProjectAdminBadge": "Administrator projektu" }, "prometheusQueryPreset": { diff --git a/resources/i18n/pt-BR.json b/resources/i18n/pt-BR.json index 5cc523a57f..75e8b1d6f2 100644 --- a/resources/i18n/pt-BR.json +++ b/resources/i18n/pt-BR.json @@ -2111,6 +2111,7 @@ "RoleMember": "Membro" }, "projectSelect": { + "NoAccessibleProjects": "Nenhum projeto acessível.", "ProjectAdminBadge": "Administrador do projeto" }, "prometheusQueryPreset": { diff --git a/resources/i18n/pt.json b/resources/i18n/pt.json index 5b491b5743..dfcd0996cc 100644 --- a/resources/i18n/pt.json +++ b/resources/i18n/pt.json @@ -2113,6 +2113,7 @@ "RoleMember": "Membro" }, "projectSelect": { + "NoAccessibleProjects": "Nenhum projeto acessível.", "ProjectAdminBadge": "Administrador do projeto" }, "prometheusQueryPreset": { diff --git a/resources/i18n/ru.json b/resources/i18n/ru.json index 7fa4ca35b9..e7ac67f805 100644 --- a/resources/i18n/ru.json +++ b/resources/i18n/ru.json @@ -2111,6 +2111,7 @@ "RoleMember": "Участник" }, "projectSelect": { + "NoAccessibleProjects": "Нет доступных проектов.", "ProjectAdminBadge": "Администратор проекта" }, "prometheusQueryPreset": { diff --git a/resources/i18n/th.json b/resources/i18n/th.json index 3c3115d922..5bb2e000dc 100644 --- a/resources/i18n/th.json +++ b/resources/i18n/th.json @@ -2113,6 +2113,7 @@ "RoleMember": "สมาชิก" }, "projectSelect": { + "NoAccessibleProjects": "ไม่มีโปรเจกต์ที่สามารถเข้าถึงได้", "ProjectAdminBadge": "ผู้ดูแลโปรเจกต์" }, "prometheusQueryPreset": { diff --git a/resources/i18n/tr.json b/resources/i18n/tr.json index 41e46542a8..472558942a 100644 --- a/resources/i18n/tr.json +++ b/resources/i18n/tr.json @@ -2111,6 +2111,7 @@ "RoleMember": "Üye" }, "projectSelect": { + "NoAccessibleProjects": "Erişilebilir proje bulunmuyor.", "ProjectAdminBadge": "Proje Yöneticisi" }, "prometheusQueryPreset": { diff --git a/resources/i18n/vi.json b/resources/i18n/vi.json index bbc6b9b9d6..92ae8bc741 100644 --- a/resources/i18n/vi.json +++ b/resources/i18n/vi.json @@ -2113,6 +2113,7 @@ "RoleMember": "Thành viên" }, "projectSelect": { + "NoAccessibleProjects": "Không có dự án nào có thể truy cập.", "ProjectAdminBadge": "Quản trị dự án" }, "prometheusQueryPreset": { diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 5001ce3904..282fd10455 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -2113,6 +2113,7 @@ "RoleMember": "成员" }, "projectSelect": { + "NoAccessibleProjects": "没有可访问的项目。", "ProjectAdminBadge": "项目管理员" }, "prometheusQueryPreset": { diff --git a/resources/i18n/zh-TW.json b/resources/i18n/zh-TW.json index f1b9841d4b..1d9da60584 100644 --- a/resources/i18n/zh-TW.json +++ b/resources/i18n/zh-TW.json @@ -2115,6 +2115,7 @@ "RoleMember": "成員" }, "projectSelect": { + "NoAccessibleProjects": "沒有可存取的專案。", "ProjectAdminBadge": "專案管理員" }, "prometheusQueryPreset": {