diff --git a/react/src/components/Chat/DeploymentSelect.tsx b/react/src/components/Chat/DeploymentSelect.tsx new file mode 100644 index 0000000000..9c05ef101f --- /dev/null +++ b/react/src/components/Chat/DeploymentSelect.tsx @@ -0,0 +1,232 @@ +import { + DeploymentSelectQuery, + DeploymentSelectQuery$data, + DeploymentFilter, +} from '../../__generated__/DeploymentSelectQuery.graphql'; +import { DeploymentSelectValueQuery } from '../../__generated__/DeploymentSelectValueQuery.graphql'; +import BAILink from '../BAILink'; +import BAISelect from '../BAISelect'; +import TotalFooter from '../TotalFooter'; +import { useControllableValue } from 'ahooks'; +import { GetRef, SelectProps, Skeleton, Tooltip } from 'antd'; +import { BAIFlex, toLocalId } from 'backend.ai-ui'; +import _ from 'lodash'; +import { InfoIcon } from 'lucide-react'; +import React, { useDeferredValue, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { graphql, useLazyLoadQuery } from 'react-relay'; +import { useRelayCursorPaginatedQuery } from 'src/hooks/useRelayCursorPaginatedQuery'; + +export type Deployment = NonNullableNodeOnEdges< + DeploymentSelectQuery$data['deployments'] +>; + +export interface DeploymentSelectProps + extends Omit { + fetchKey?: string; + filter?: DeploymentFilter; +} + +const DeploymentSelect: React.FC = ({ + fetchKey, + filter, + loading, + ...selectPropsWithoutLoading +}) => { + const { t } = useTranslation(); + const [controllableValue, setControllableValue] = useControllableValue< + string | undefined + >(selectPropsWithoutLoading); + const [controllableOpen, setControllableOpen] = useControllableValue( + selectPropsWithoutLoading, + { + valuePropName: 'open', + trigger: 'onDropdownVisibleChange', + }, + ); + const deferredOpen = useDeferredValue(controllableOpen); + const [searchStr, setSearchStr] = useState(); + const deferredSearchStr = useDeferredValue(searchStr); + + const selectRef = useRef | null>(null); + + // Query for selected deployment details + const { deployment: selectedDeployment } = + useLazyLoadQuery( + graphql` + query DeploymentSelectValueQuery($id: ID!) { + deployment(id: $id) { + id + metadata { + name + status + createdAt + } + } + } + `, + { + id: controllableValue ?? '', + }, + { + // to skip the query when controllableValue is empty + fetchPolicy: controllableValue ? 'store-or-network' : 'store-only', + }, + ); + + // Paginated deployments query (cursor-based) + const { + paginationData, + result: { deployments }, + loadNext, + isLoadingNext, + } = useRelayCursorPaginatedQuery( + graphql` + query DeploymentSelectQuery( + $first: Int + $after: String + $filter: DeploymentFilter + $orderBy: [DeploymentOrderBy!] + ) { + deployments( + first: $first + after: $after + filter: $filter + orderBy: $orderBy + ) { + count + pageInfo { + hasNextPage + endCursor + } + edges { + node { + id + metadata { + name + status + createdAt + } + } + } + } + } + `, + { first: 10 }, + { + filter, + ...(deferredSearchStr + ? { + filter: { + ...filter, + name: { iContains: `%${deferredSearchStr}%` }, + }, + } + : {}), + }, + { + fetchKey, + fetchPolicy: deferredOpen ? 'network-only' : 'store-only', + }, + { + // getTotal: (result) => result.deployments?.count, + getItem: (result) => result.deployments?.edges?.map((e) => e.node), + getPageInfo: (result) => { + const pageInfo = result.deployments?.pageInfo; + return { + hasNextPage: pageInfo?.hasNextPage ?? false, + endCursor: pageInfo?.endCursor ?? undefined, + }; + }, + getId: (item: Deployment) => item?.id, + }, + ); + + const selectOptions = _.map(paginationData, (item: Deployment) => { + return { + label: item?.metadata?.name, + value: item?.id, + deployment: item, + }; + }); + + const [optimisticValueWithLabel, setOptimisticValueWithLabel] = useState( + selectedDeployment + ? { + label: selectedDeployment?.metadata?.name || undefined, + value: selectedDeployment?.id || undefined, + } + : controllableValue + ? { + label: controllableValue, + value: controllableValue, + } + : controllableValue, + ); + + const isValueMatched = searchStr === deferredSearchStr; + useEffect(() => { + if (isValueMatched) { + selectRef.current?.scrollTo(0); + } + }, [isValueMatched]); + + return ( + { + setSearchStr(v); + }} + labelRender={({ label }: { label: React.ReactNode }) => { + return label ? ( + + {label} + + + + + + + ) : ( + label + ); + }} + autoClearSearchValue + filterOption={false} + loading={searchStr !== deferredSearchStr || loading} + options={selectOptions} + {...selectPropsWithoutLoading} + // override value and onChange + labelInValue // use labelInValue to display the selected option label + value={optimisticValueWithLabel} + onChange={(v, option) => { + setOptimisticValueWithLabel(v); + setControllableValue(v.value, option); + selectPropsWithoutLoading.onChange?.(v.value || '', option); + }} + endReached={() => { + loadNext(); + }} + open={controllableOpen} + onDropdownVisibleChange={setControllableOpen} + notFoundContent={ + _.isUndefined(paginationData) ? ( + + ) : undefined + } + footer={ + _.isNumber(deployments?.count) && deployments.count > 0 ? ( + + ) : undefined + } + /> + ); +}; + +export default DeploymentSelect; diff --git a/react/src/hooks/useRelayCursorPaginatedQuery.ts b/react/src/hooks/useRelayCursorPaginatedQuery.ts new file mode 100644 index 0000000000..159cc891ac --- /dev/null +++ b/react/src/hooks/useRelayCursorPaginatedQuery.ts @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import { useState, useRef, useMemo, useTransition, useEffect } from 'react'; +import { GraphQLTaggedNode, useLazyLoadQuery } from 'react-relay'; +import type { OperationType } from 'relay-runtime'; + +/** + * Cursor-based pagination hook for Relay-compliant GraphQL connections. + * Supports queries with `first`, `after`, `last`, `before`, etc. + */ +export type CursorOptions = { + getItem: (result: Result) => ItemType[] | undefined; + // getTotal: (result: Result) => number | undefined; + getPageInfo: (result: Result) => { + hasNextPage: boolean; + endCursor?: string; + hasPreviousPage?: boolean; + startCursor?: string; + }; + getId: (item: ItemType) => string | undefined | null; +}; + +export function useRelayCursorPaginatedQuery( + query: GraphQLTaggedNode, + initialPaginationVariables: { first?: number; last?: number }, + otherVariables: Omit< + Partial, + 'first' | 'last' | 'after' | 'before' + >, + options: Parameters>[2], + { + getItem, + getId, + // getTotal, + getPageInfo, + }: CursorOptions, +) { + const [cursor, setCursor] = useState(undefined); + const [isLoadingNext, startLoadingNextTransition] = useTransition(); + const previousResult = useRef([]); + + const previousOtherVariablesRef = useRef(otherVariables); + + const isNewOtherVariables = !_.isEqual( + previousOtherVariablesRef.current, + otherVariables, + ); + + const variables = { + ...initialPaginationVariables, + ...otherVariables, + after: cursor, + }; + + const result = useLazyLoadQuery(query, variables, options); + + const data = useMemo(() => { + const items = getItem(result); + return items + ? _.uniqBy([...previousResult.current, ...items], getId) + : undefined; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const pageInfo = getPageInfo(result); + const hasNext = pageInfo.hasNextPage; + + const loadNext = () => { + if (isLoadingNext || !hasNext) return; + previousResult.current = data || []; + startLoadingNextTransition(() => { + setCursor(pageInfo.endCursor); + }); + }; + + useEffect(() => { + // Reset cursor when otherVariables change + if (isNewOtherVariables) { + previousResult.current = []; + setCursor(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isNewOtherVariables]); + + return { + paginationData: data, + result, + loadNext, + hasNext, + isLoadingNext, + }; +} diff --git a/resources/i18n/de.json b/resources/i18n/de.json index 3b618c453b..e8dae66982 100644 --- a/resources/i18n/de.json +++ b/resources/i18n/de.json @@ -497,6 +497,9 @@ "Used": "gebraucht" } }, + "deployment": { + "SelectDeployment": "Wählen Sie Bereitstellung" + }, "desktopNotification": { "NotSupported": "Dieser Browser unterstützt keine Benachrichtigungen.", "PermissionDenied": "Sie haben den Benachrichtigungszugriff abgelehnt. \nUm Warnungen zu verwenden, lassen Sie diese bitte in Ihren Browsereinstellungen zu." diff --git a/resources/i18n/el.json b/resources/i18n/el.json index 48d3ee2c1c..5d3a9838ca 100644 --- a/resources/i18n/el.json +++ b/resources/i18n/el.json @@ -495,6 +495,9 @@ "Used": "χρησιμοποιημένο" } }, + "deployment": { + "SelectDeployment": "Επιλέξτε την ανάπτυξη" + }, "desktopNotification": { "NotSupported": "Αυτό το πρόγραμμα περιήγησης δεν υποστηρίζει ειδοποιήσεις.", "PermissionDenied": "Έχετε αρνηθεί την πρόσβαση ειδοποίησης. \nΓια να χρησιμοποιήσετε ειδοποιήσεις, αφήστε το στις ρυθμίσεις του προγράμματος περιήγησής σας." diff --git a/resources/i18n/en.json b/resources/i18n/en.json index e6a64dea10..b183443f17 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -594,6 +594,7 @@ "RoutesInfo": "Routes Info", "RoutingID": "Routing ID", "ScalingSettings": "Scaling Settings", + "SelectDeployment": "Select Deployment", "SessionID": "Session ID", "SetAsActiveRevision": "Set as active revision", "SetAutoScalingRule": "Set Auto Scaling Rule", diff --git a/resources/i18n/es.json b/resources/i18n/es.json index 807d1fb76f..aba4d0938d 100644 --- a/resources/i18n/es.json +++ b/resources/i18n/es.json @@ -497,6 +497,9 @@ "Used": "usado" } }, + "deployment": { + "SelectDeployment": "Seleccionar implementación" + }, "desktopNotification": { "NotSupported": "Este navegador no admite notificaciones.", "PermissionDenied": "Has negado el acceso a la notificación. \nPara usar alertas, permítelo en la configuración de su navegador." diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json index fa929166ca..3633e90917 100644 --- a/resources/i18n/fi.json +++ b/resources/i18n/fi.json @@ -497,6 +497,9 @@ "Used": "käytetty" } }, + "deployment": { + "SelectDeployment": "Valitse käyttöönotto" + }, "desktopNotification": { "NotSupported": "Tämä selain ei tue ilmoituksia.", "PermissionDenied": "Olet kiistänyt ilmoituskäytön. \nJos haluat käyttää hälytyksiä, salli se selaimen asetuksissa." diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 22dfb04996..6174f63597 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -497,6 +497,9 @@ "Used": "utilisé" } }, + "deployment": { + "SelectDeployment": "Sélectionnez le déploiement" + }, "desktopNotification": { "NotSupported": "Ce navigateur ne prend pas en charge les notifications.", "PermissionDenied": "Vous avez nié l'accès à la notification. \nPour utiliser des alertes, veuillez le permettre dans les paramètres de votre navigateur." diff --git a/resources/i18n/id.json b/resources/i18n/id.json index 7636297ed6..f3a111348b 100644 --- a/resources/i18n/id.json +++ b/resources/i18n/id.json @@ -496,6 +496,9 @@ "Used": "digunakan" } }, + "deployment": { + "SelectDeployment": "Pilih penempatan" + }, "desktopNotification": { "NotSupported": "Browser ini tidak mendukung pemberitahuan.", "PermissionDenied": "Anda telah menolak akses pemberitahuan. \nUntuk menggunakan lansiran, harap diizinkan di pengaturan browser Anda." diff --git a/resources/i18n/it.json b/resources/i18n/it.json index 382f70cddb..f2d854cc3d 100644 --- a/resources/i18n/it.json +++ b/resources/i18n/it.json @@ -496,6 +496,9 @@ "Used": "usato" } }, + "deployment": { + "SelectDeployment": "Seleziona la distribuzione" + }, "desktopNotification": { "NotSupported": "Questo browser non supporta le notifiche.", "PermissionDenied": "Hai negato l'accesso alla notifica. \nPer utilizzare gli avvisi, consentirlo nelle impostazioni del browser." diff --git a/resources/i18n/ja.json b/resources/i18n/ja.json index d01687b393..ecf463bab3 100644 --- a/resources/i18n/ja.json +++ b/resources/i18n/ja.json @@ -496,6 +496,9 @@ "Used": "使用中" } }, + "deployment": { + "SelectDeployment": "展開を選択します" + }, "desktopNotification": { "NotSupported": "このブラウザは通知をサポートしていません。", "PermissionDenied": "通知アクセスを拒否しました。\nアラートを使用するには、ブラウザの設定で許可してください。" diff --git a/resources/i18n/ko.json b/resources/i18n/ko.json index 0e488754b1..1d1733a7f3 100644 --- a/resources/i18n/ko.json +++ b/resources/i18n/ko.json @@ -499,6 +499,9 @@ "Used": "사용 중" } }, + "deployment": { + "SelectDeployment": "디플로이먼트 선택" + }, "desktopNotification": { "NotSupported": "현재 브라우저에서는 알림 기능을 지원하지 않습니다. ", "PermissionDenied": "알림 권한이 거부되었습니다. 브라우저 설정에서 알림을 허용해주세요." diff --git a/resources/i18n/mn.json b/resources/i18n/mn.json index 8239b50140..9f21b801fb 100644 --- a/resources/i18n/mn.json +++ b/resources/i18n/mn.json @@ -495,6 +495,9 @@ "Used": "ашигласан" } }, + "deployment": { + "SelectDeployment": "Байршуулалтыг сонгоно уу" + }, "desktopNotification": { "NotSupported": "Энэ хөтөч нь мэдэгдлийг дэмждэггүй.", "PermissionDenied": "Та мэдэгдлийн хандалтыг үгүйсгэж байна. \nАнхааруулга ашиглахын тулд үүнийг өөрийн хөтөчийн тохиргоонд оруулна уу." diff --git a/resources/i18n/ms.json b/resources/i18n/ms.json index a500639dc5..51bbff5ec1 100644 --- a/resources/i18n/ms.json +++ b/resources/i18n/ms.json @@ -496,6 +496,9 @@ "Used": "digunakan" } }, + "deployment": { + "SelectDeployment": "Pilih penyebaran" + }, "desktopNotification": { "NotSupported": "Penyemak imbas ini tidak menyokong pemberitahuan.", "PermissionDenied": "Anda telah menafikan akses pemberitahuan. \nUntuk menggunakan makluman, sila berikannya dalam tetapan penyemak imbas anda." diff --git a/resources/i18n/pl.json b/resources/i18n/pl.json index a10bbb9858..469b68438c 100644 --- a/resources/i18n/pl.json +++ b/resources/i18n/pl.json @@ -497,6 +497,9 @@ "Used": "używany" } }, + "deployment": { + "SelectDeployment": "Wybierz wdrożenie" + }, "desktopNotification": { "NotSupported": "Ta przeglądarka nie obsługuje powiadomień.", "PermissionDenied": "Odmówiłeś dostępu do powiadomień. \nAby użyć alertów, pozwól temu w ustawieniach przeglądarki." diff --git a/resources/i18n/pt-BR.json b/resources/i18n/pt-BR.json index 9ee09d0b0e..f52147426d 100644 --- a/resources/i18n/pt-BR.json +++ b/resources/i18n/pt-BR.json @@ -497,6 +497,9 @@ "Used": "utilizado" } }, + "deployment": { + "SelectDeployment": "Selecione a implantação" + }, "desktopNotification": { "NotSupported": "Este navegador não suporta notificações.", "PermissionDenied": "Você negou acesso à notificação. \nPara usar alertas, deixe -o nas configurações do navegador." diff --git a/resources/i18n/pt.json b/resources/i18n/pt.json index bcc251e77f..2e634e0268 100644 --- a/resources/i18n/pt.json +++ b/resources/i18n/pt.json @@ -497,6 +497,9 @@ "Used": "utilizado" } }, + "deployment": { + "SelectDeployment": "Selecione a implantação" + }, "desktopNotification": { "NotSupported": "Este navegador não suporta notificações.", "PermissionDenied": "Você negou acesso à notificação. \nPara usar alertas, deixe -o nas configurações do navegador." diff --git a/resources/i18n/ru.json b/resources/i18n/ru.json index f36a7643e3..5fcd02be7c 100644 --- a/resources/i18n/ru.json +++ b/resources/i18n/ru.json @@ -497,6 +497,9 @@ "Used": "используется" } }, + "deployment": { + "SelectDeployment": "Выберите развертывание" + }, "desktopNotification": { "NotSupported": "Этот браузер не поддерживает уведомления.", "PermissionDenied": "Вы отрицали доступ уведомлений. \nЧтобы использовать оповещения, позвольте им в настройках браузера." diff --git a/resources/i18n/th.json b/resources/i18n/th.json index d79c2d124f..e334c38243 100644 --- a/resources/i18n/th.json +++ b/resources/i18n/th.json @@ -491,6 +491,9 @@ "Used": "ใช้แล้ว" } }, + "deployment": { + "SelectDeployment": "เลือกการปรับใช้" + }, "desktopNotification": { "NotSupported": "เบราว์เซอร์นี้ไม่รองรับการแจ้งเตือน", "PermissionDenied": "คุณปฏิเสธการเข้าถึงการแจ้งเตือน \nหากต้องการใช้การแจ้งเตือนโปรดรอการตั้งค่าเบราว์เซอร์ของคุณ" diff --git a/resources/i18n/tr.json b/resources/i18n/tr.json index ed6bb24762..375b290b84 100644 --- a/resources/i18n/tr.json +++ b/resources/i18n/tr.json @@ -497,6 +497,9 @@ "Used": "kullanılmış" } }, + "deployment": { + "SelectDeployment": "Dağıtım'ı seçin" + }, "desktopNotification": { "NotSupported": "Bu tarayıcı bildirimleri desteklemez.", "PermissionDenied": "Bildirim erişimini reddettiniz. \nUyarıları kullanmak için lütfen tarayıcı ayarlarınıza izin verin." diff --git a/resources/i18n/vi.json b/resources/i18n/vi.json index 0f00a17ca3..918311df4e 100644 --- a/resources/i18n/vi.json +++ b/resources/i18n/vi.json @@ -497,6 +497,9 @@ "Used": "đã sử dụng" } }, + "deployment": { + "SelectDeployment": "Chọn triển khai" + }, "desktopNotification": { "NotSupported": "Trình duyệt này không hỗ trợ thông báo.", "PermissionDenied": "Bạn đã từ chối truy cập thông báo. \nĐể sử dụng cảnh báo, vui lòng cho phép nó trong cài đặt trình duyệt của bạn." diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index c6587d5a82..93322c5f65 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -497,6 +497,9 @@ "Used": "中古" } }, + "deployment": { + "SelectDeployment": "选择部署" + }, "desktopNotification": { "NotSupported": "该浏览器不支持通知。", "PermissionDenied": "您否认通知访问。\n要使用警报,请在浏览器设置中允许它。"