diff --git a/data/schema.graphql b/data/schema.graphql index 7fa786903b..0c5fe5a88b 100644 --- a/data/schema.graphql +++ b/data/schema.graphql @@ -140,6 +140,23 @@ Added in 24.12.0. One of ['read_attribute', 'update_attribute', 'create_compute_ scalar AgentPermissionField @join__type(graph: GRAPHENE) +"""Added in 25.15.0""" +type AgentResource + @join__type(graph: STRAWBERRY) +{ + free: JSON! + used: JSON! + capacity: JSON! +} + +"""Added in 25.15.0""" +type AgentStats + @join__type(graph: STRAWBERRY) +{ + """Added in 25.15.0""" + totalResource: AgentResource! +} + """A schema for normal users.""" type AgentSummary implements Item @join__implements(graph: GRAPHENE, interface: "Item") @@ -295,6 +312,47 @@ type ArtifactRegistry type: ArtifactRegistryType! } +""" +Added in 25.15.0. + +Represents common metadata for an artifact registry. +All artifact registry nodes expose that information regardless of type. +""" +type ArtifactRegistryMeta implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + name: String! + registryId: ID! + type: ArtifactRegistryType! + url: String! +} + +"""Added in 25.15.0""" +type ArtifactRegistryMetaConnection + @join__type(graph: STRAWBERRY) +{ + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [ArtifactRegistryMetaEdge!]! + count: Int! +} + +"""An edge in a connection.""" +type ArtifactRegistryMetaEdge + @join__type(graph: STRAWBERRY) +{ + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: ArtifactRegistryMeta! +} + enum ArtifactRegistryType @join__type(graph: STRAWBERRY) { @@ -302,6 +360,13 @@ enum ArtifactRegistryType RESERVOIR @join__enumValue(graph: STRAWBERRY) } +enum ArtifactRemoteStatus + @join__type(graph: STRAWBERRY) +{ + SCANNED @join__enumValue(graph: STRAWBERRY) + AVAILABLE @join__enumValue(graph: STRAWBERRY) +} + """Added in 25.14.0""" type ArtifactRevision implements Node @join__implements(graph: STRAWBERRY, interface: "Node") @@ -310,6 +375,9 @@ type ArtifactRevision implements Node """The Globally Unique ID of this object""" id: ID! status: ArtifactStatus! + + """Added in 25.15.0""" + remoteStatus: ArtifactRemoteStatus version: String! readme: String size: ByteSize @@ -1401,6 +1469,9 @@ type CreateUser ok: Boolean msg: String user: User + + """Added in 25.15.0.""" + keypair: KeyPair } type CreateUserResourcePolicy @@ -1452,6 +1523,87 @@ type DealiasImage msg: String } +"""Added in 25.15.0""" +input DelegateeTarget + @join__type(graph: STRAWBERRY) +{ + delegateeReservoirId: ID! + targetRegistryId: ID! +} + +""" +Added in 25.15.0. + +Input type for delegated import of artifact revisions from a reservoir registry's remote registry. +Used to specify which artifact revisions should be imported from the remote registry source +into the local reservoir registry storage. +""" +input DelegateImportArtifactsInput + @join__type(graph: STRAWBERRY) +{ + """List of artifact revision IDs of delegatee artifact registry""" + artifactRevisionIds: [ID!]! +} + +""" +Added in 25.15.0. + +Response payload for delegated artifact import operation. +Contains the imported artifact revisions and associated background tasks. +The tasks can be monitored to track the progress of the import operation. +""" +type DelegateImportArtifactsPayload + @join__type(graph: STRAWBERRY) +{ + """ + Connection of artifact revisions that were imported from the reservoir registry's remote registry + """ + artifactRevisions: ArtifactRevisionConnection! + + """List of background tasks created for importing the artifact revisions""" + tasks: [ArtifactRevisionImportTask!]! +} + +""" +Added in 25.15.0. + +Input type for delegated scanning of artifacts from a delegatee reservoir registry's remote registry. +""" +input DelegateScanArtifactsInput + @join__type(graph: STRAWBERRY) +{ + """ID of the reservoir registry to delegate the scan request to""" + delegatorReservoirId: ID = null + + """Target delegatee reservoir registry and its remote registry to scan""" + delegateeTarget: DelegateeTarget = null + + """Maximum number of artifacts to scan (max: 500)""" + limit: Int! + + """Filter artifacts by type (e.g., model, image, package)""" + artifactType: ArtifactType = null + + """Search term to filter artifacts by name or description""" + search: String = null +} + +""" +Added in 25.15.0. + +Response payload for delegated artifact scanning operation. +Contains the list of artifacts discovered during the scan of a reservoir registry's remote registry. +These artifacts are now available for import or direct use. +""" +type DelegateScanArtifactsPayload + @join__type(graph: STRAWBERRY) +{ + """ + List of artifacts discovered during the delegated scan from the reservoir registry's remote registry + """ + artifacts: [Artifact!]! +} + """Added in 25.15.0""" input DeleteArtifactsInput @join__type(graph: STRAWBERRY) @@ -2429,6 +2581,13 @@ enum join__Graph { STRAWBERRY @join__graph(name: "strawberry", url: "http://host.docker.internal:8091/admin/gql/strawberry") } +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). +""" +scalar JSON + @join__type(graph: STRAWBERRY) + @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") + """ Allows use of a JSON String for input / output from the GraphQL schema. @@ -3766,6 +3925,37 @@ type Mutation """Added in 25.14.0""" importArtifacts(input: ImportArtifactsInput!): ImportArtifactsPayload! @join__field(graph: STRAWBERRY) + """ + Added in 25.15.0. + + Triggers artifact scanning on a remote reservoir registry. + + This mutation instructs a reservoir-type registry to initiate a scan of artifacts + from its associated remote reservoir registry source. The scan process will discover and + catalog artifacts available in the remote reservoir, making them accessible + through the local reservoir registry. + + Requirements: + - The delegator registry must be of type 'reservoir' + - The delegator reservoir registry must have a valid remote registry configuration + """ + delegateScanArtifacts(input: DelegateScanArtifactsInput!): DelegateScanArtifactsPayload! @join__field(graph: STRAWBERRY) + + """ + Added in 25.15.0. + + Trigger import of artifact revisions from a remote reservoir registry. + + This mutation instructs a reservoir-type registry to import specific artifact revisions + that were previously discovered during a scan from its remote registry. + Note that this operation does not import the artifacts directly into the local registry, but only into the delegator reservoir's storage. + + Requirements: + - The delegator registry must be of type 'reservoir' + - The delegator registry must have a valid remote registry configuration + """ + delegateImportArtifacts(input: DelegateImportArtifactsInput!): DelegateImportArtifactsPayload! @join__field(graph: STRAWBERRY) + """Added in 25.14.0""" updateArtifact(input: UpdateArtifactInput!): UpdateArtifactPayload! @join__field(graph: STRAWBERRY) @@ -3794,11 +3984,19 @@ type Mutation """Added in 25.14.0""" deleteObjectStorage(input: DeleteObjectStorageInput!): DeleteObjectStoragePayload! @join__field(graph: STRAWBERRY) - """Added in 25.14.0""" - registerObjectStorageBucket(input: RegisterObjectStorageBucketInput!): RegisterObjectStorageBucketPayload! @join__field(graph: STRAWBERRY) + """ + Added in 25.15.0. + + Registers a new namespace within a storage. + """ + registerStorageNamespace(input: RegisterStorageNamespaceInput!): RegisterStorageNamespacePayload! @join__field(graph: STRAWBERRY) - """Added in 25.14.0""" - unregisterObjectStorageBucket(input: UnregisterObjectStorageBucketInput!): UnregisterObjectStorageBucketPayload! @join__field(graph: STRAWBERRY) + """ + Added in 25.15.0. + + Unregisters an existing namespace from a storage. + """ + unregisterStorageNamespace(input: UnregisterStorageNamespaceInput!): UnregisterStorageNamespacePayload! @join__field(graph: STRAWBERRY) """Added in 25.14.0""" createHuggingfaceRegistry(input: CreateHuggingFaceRegistryInput!): CreateHuggingFaceRegistryPayload! @join__field(graph: STRAWBERRY) @@ -3899,7 +4097,7 @@ type ObjectStorage implements Node secretKey: String! endpoint: String! region: String! - namespaces(before: String, after: String, first: Int, last: Int, limit: Int, offset: Int): ObjectStorageNamespaceConnection! + namespaces(before: String, after: String, first: Int, last: Int, limit: Int, offset: Int): StorageNamespaceConnection! } """Added in 25.14.0""" @@ -3925,40 +4123,6 @@ type ObjectStorageEdge node: ObjectStorage! } -"""Added in 25.14.0""" -type ObjectStorageNamespace implements Node - @join__implements(graph: STRAWBERRY, interface: "Node") - @join__type(graph: STRAWBERRY) -{ - """The Globally Unique ID of this object""" - id: ID! - storageId: ID! - bucket: String! -} - -"""Added in 25.14.0""" -type ObjectStorageNamespaceConnection - @join__type(graph: STRAWBERRY) -{ - """Pagination data for this connection""" - pageInfo: PageInfo! - - """Contains the nodes in this connection""" - edges: [ObjectStorageNamespaceEdge!]! - count: Int! -} - -"""An edge in a connection.""" -type ObjectStorageNamespaceEdge - @join__type(graph: STRAWBERRY) -{ - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: ObjectStorageNamespace! -} - enum OrderDirection @join__type(graph: STRAWBERRY) { @@ -4572,6 +4736,9 @@ type Query """Added in 25.14.0""" defaultArtifactRegistry(artifactType: ArtifactType!): ArtifactRegistry @join__field(graph: STRAWBERRY) + + """Added in 25.15.0""" + agentStats: AgentStats! @join__field(graph: STRAWBERRY) } type QuotaDetails @@ -4605,16 +4772,24 @@ type RawServiceConfig extraCliParameters: String } -"""Added in 25.14.0""" -input RegisterObjectStorageBucketInput +""" +Added in 25.15.0. + +Input type for registering a storage namespace. +""" +input RegisterStorageNamespaceInput @join__type(graph: STRAWBERRY) { storageId: UUID! - bucketName: String! + namespace: String! } -"""Added in 25.14.0""" -type RegisterObjectStorageBucketPayload +""" +Added in 25.15.0. + +Payload returned after storage namespace registration. +""" +type RegisterStorageNamespacePayload @join__type(graph: STRAWBERRY) { id: UUID! @@ -4681,6 +4856,7 @@ type ReservoirRegistry implements Node accessKey: String! secretKey: String! apiVersion: String! + remoteArtifactRegistries: ArtifactRegistryMetaConnection! } """Added in 25.14.0""" @@ -5062,6 +5238,53 @@ type SourceInfo url: String } +""" +Added in 25.15.0. + +Storage namespace provides logical separation of data within a single storage system +to organize and isolate domain-specific concerns. + +Implementation varies by storage type: +- Object Storage (S3, MinIO): Uses bucket-based namespace separation +- File System (VFS): Uses directory path prefix for namespace distinction +""" +type StorageNamespace implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + storageId: ID! + namespace: String! +} + +""" +Added in 25.15.0. + +Storage namespace connection for pagination. +""" +type StorageNamespaceConnection + @join__type(graph: STRAWBERRY) +{ + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [StorageNamespaceEdge!]! + count: Int! +} + +"""An edge in a connection.""" +type StorageNamespaceEdge + @join__type(graph: STRAWBERRY) +{ + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: StorageNamespace! +} + type StorageVolume implements Item @join__implements(graph: GRAPHENE, interface: "Item") @join__type(graph: GRAPHENE) @@ -5135,16 +5358,24 @@ type UnloadImage task_id: String } -"""Added in 25.14.0""" -input UnregisterObjectStorageBucketInput +""" +Added in 25.15.0. + +Input type for unregistering a storage namespace. +""" +input UnregisterStorageNamespaceInput @join__type(graph: STRAWBERRY) { storageId: UUID! - bucketName: String! + namespace: String! } -"""Added in 25.14.0""" -type UnregisterObjectStorageBucketPayload +""" +Added in 25.15.0. + +Payload returned after storage namespace unregistration. +""" +type UnregisterStorageNamespacePayload @join__type(graph: STRAWBERRY) { id: UUID! diff --git a/packages/backend.ai-ui/src/components/fragments/BAIBucketSelect.tsx b/packages/backend.ai-ui/src/components/fragments/BAIBucketSelect.tsx index 79e217d27f..e5066e1dda 100644 --- a/packages/backend.ai-ui/src/components/fragments/BAIBucketSelect.tsx +++ b/packages/backend.ai-ui/src/components/fragments/BAIBucketSelect.tsx @@ -66,7 +66,7 @@ const BAIBucketSelect = ({ edges { node { id - bucket + namespace } } } @@ -95,7 +95,7 @@ const BAIBucketSelect = ({ ); const selectedOptions = _.map(paginationData, (item) => ({ - label: item.node.bucket, + label: item.node.namespace, value: item.node.id, })); diff --git a/react/src/components/ReservoirArtifactsList.tsx b/react/src/components/ReservoirArtifactsList.tsx new file mode 100644 index 0000000000..cd27ab00d8 --- /dev/null +++ b/react/src/components/ReservoirArtifactsList.tsx @@ -0,0 +1,460 @@ +import BAIFetchKeyButton from './BAIFetchKeyButton'; +import BAIRadioGroup from './BAIRadioGroup'; +import { useToggle } from 'ahooks'; +import { Button, theme, Tooltip, Typography } from 'antd'; +import { + BAIActivateArtifactsModal, + BAIActivateArtifactsModalArtifactsFragmentKey, + BAIArtifactTable, + BAIDeactivateArtifactsModal, + BAIDeactivateArtifactsModalArtifactsFragmentKey, + BAIFlex, + BAIGraphQLPropertyFilter, + BAIHuggingFaceIcon, + BAIImportArtifactModal, + BAIImportArtifactModalArtifactFragmentKey, + BAIImportArtifactModalArtifactRevisionFragmentKey, + BAIImportFromHuggingFaceModal, + toLocalId, +} from 'backend.ai-ui'; +import _ from 'lodash'; +import { BanIcon, UndoIcon } from 'lucide-react'; +import { useDeferredValue, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { graphql, useLazyLoadQuery } from 'react-relay'; +import { useNavigate } from 'react-router-dom'; +import { + ReservoirArtifactsListQuery, + ReservoirArtifactsListQuery$data, + ReservoirArtifactsListQuery$variables, +} from 'src/__generated__/ReservoirArtifactsListQuery.graphql'; +import { INITIAL_FETCH_KEY, useUpdatableState } from 'src/hooks'; +import { useBAIPaginationOptionStateOnSearchParam } from 'src/hooks/reactPaginationQueryOptions'; +import { useSetBAINotification } from 'src/hooks/useBAINotification'; +import { + JsonParam, + StringParam, + useQueryParams, + withDefault, +} from 'use-query-params'; + +const getStatusFilter = (status: string) => { + return { availability: [status] }; +}; + +type ArtifactNode = NonNullable< + ReservoirArtifactsListQuery$data['artifacts'] +>['edges'][number]['node']; + +const ReservoirArtifactsList = () => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const navigate = useNavigate(); + const { upsertNotification } = useSetBAINotification(); + + const [selectedArtifactIdList, setSelectedArtifactIdList] = useState< + { + id: string; + data: ArtifactNode; + }[] + >([]); + const [selectedArtifacts, setSelectedArtifacts] = + useState([]); + const [selectedRestoreArtifacts, setSelectedRestoreArtifacts] = + useState([]); + const [selectedArtifact, setSelectedArtifact] = + useState(null); + const [selectedRevision, setSelectedRevision] = + useState([]); + const [openHuggingFaceModal, { toggle: toggleOpenHuggingFaceModal }] = + useToggle(); + + const { + baiPaginationOption, + tablePaginationOption, + setTablePaginationOption, + } = useBAIPaginationOptionStateOnSearchParam({ + current: 1, + pageSize: 10, + }); + + const [queryParams, setQuery] = useQueryParams({ + filter: withDefault(JsonParam, {}), + mode: withDefault(StringParam, 'ALIVE'), + }); + const jsonStringFilter = JSON.stringify(queryParams.filter); + + const queryVariables: ReservoirArtifactsListQuery$variables = useMemo( + () => ({ + offset: baiPaginationOption.offset, + limit: baiPaginationOption.limit, + order: [ + { + field: 'UPDATED_AT', + direction: 'DESC', + }, + ], + filter: _.merge( + {}, + JSON.parse(jsonStringFilter || '{}'), + getStatusFilter(queryParams.mode), + ), + }), + [ + baiPaginationOption.offset, + baiPaginationOption.limit, + jsonStringFilter, + queryParams.mode, + ], + ); + const deferredQueryVariables = useDeferredValue(queryVariables); + + const [fetchKey, updateFetchKey] = useUpdatableState(INITIAL_FETCH_KEY); + const deferredFetchKey = useDeferredValue(fetchKey); + + // const [rescanArtifacts, isInflightRescanArtifacts] = + // useMutation(graphql` + // mutation ReservoirPageRescanArtifactsMutation( + // $input: ScanArtifactsInput! + // ) { + // scanArtifacts(input: $input) { + // artifacts { + // id + // } + // } + // } + // `); + + const queryRef = useLazyLoadQuery( + graphql` + query ReservoirArtifactsListQuery( + $order: [ArtifactOrderBy!] + $limit: Int! + $offset: Int! + $filter: ArtifactFilter! + ) { + defaultArtifactRegistry(artifactType: MODEL) { + name + type + } + artifacts( + orderBy: $order + limit: $limit + offset: $offset + filter: $filter + ) { + count + edges { + node { + id + ...BAIArtifactTableArtifactFragment + ...BAIImportArtifactModalArtifactFragment + ...BAIDeactivateArtifactsModalArtifactsFragment + ...BAIActivateArtifactsModalArtifactsFragment + revisions( + first: 1 + orderBy: { field: VERSION, direction: DESC } + ) { + edges { + node { + id + ...BAIImportArtifactModalArtifactRevisionFragment + } + } + } + } + } + } + } + `, + deferredQueryVariables, + { + fetchKey: + deferredFetchKey === INITIAL_FETCH_KEY ? undefined : deferredFetchKey, + fetchPolicy: + deferredFetchKey === INITIAL_FETCH_KEY + ? 'store-and-network' + : 'network-only', + }, + ); + + const { artifacts, defaultArtifactRegistry } = queryRef; + const isAvailableUsingHuggingFace = + defaultArtifactRegistry?.type === 'HUGGINGFACE'; + const mode = queryParams.mode; + return ( + <> + + + + { + setQuery({ mode: e.target.value }, 'replaceIn'); + setTablePaginationOption({ current: 1 }); + setSelectedArtifactIdList([]); + setSelectedArtifacts([]); + setSelectedRestoreArtifacts([]); + }} + /> + { + setQuery({ filter: value ?? {} }, 'replaceIn'); + }} + value={queryParams.filter} + filterProperties={[ + { + fixedOperator: 'contains', + key: 'name', + propertyLabel: t('reservoirPage.Name'), + type: 'string', + }, + { + fixedOperator: 'contains', + key: 'source', + propertyLabel: t('reservoirPage.Source'), + type: 'string', + }, + { + fixedOperator: 'contains', + key: 'registry', + propertyLabel: t('reservoirPage.Registry'), + type: 'string', + }, + ]} + /> + + + {selectedArtifactIdList.length > 0 && ( + + + {t('general.NSelected', { + count: selectedArtifactIdList.length, + })} + + + + )} + + + edge.node)} + onClickPull={(artifactId: string, revisionId: string) => { + artifacts.edges.forEach((artifact) => { + if (artifact.node.id === artifactId) { + setSelectedArtifact(artifact.node); + artifact.node.revisions.edges.forEach((revision) => { + if (revision.node.id === revisionId) { + setSelectedRevision([revision.node]); + return; + } + }); + return; + } + }); + }} + onClickDelete={(artifactId: string) => { + artifacts.edges.forEach((edge) => { + if (edge.node.id === artifactId) { + setSelectedArtifacts([edge.node]); + return; + } + }); + }} + onClickRestore={(artifactId: string) => { + artifacts.edges.forEach((edge) => { + if (edge.node.id === artifactId) { + setSelectedRestoreArtifacts([edge.node]); + return; + } + }); + }} + rowSelection={{ + type: 'checkbox', + onChange: (keys) => { + const artifactIdList = artifacts.edges.map((e) => e.node.id); + setSelectedArtifactIdList((prev) => { + const _filtered = prev.filter( + (v) => !artifactIdList.includes(v.id), + ); + const _selected = artifacts.edges + .filter((e) => keys.includes(e.node.id)) + .map((arr) => ({ + id: arr.node.id, + data: arr.node, + })); + return _filtered.concat(_selected); + }); + }, + selectedRowKeys: selectedArtifactIdList.map((v) => v.id), + }} + loading={deferredQueryVariables !== queryVariables} + pagination={{ + pageSize: tablePaginationOption.pageSize, + current: tablePaginationOption.current, + total: artifacts.count, + onChange: (current, pageSize) => { + if (_.isNumber(current) && _.isNumber(pageSize)) { + setTablePaginationOption({ current, pageSize }); + } + }, + }} + /> + + { + setSelectedArtifact(null); + setSelectedRevision([]); + tasks.forEach((task) => { + upsertNotification({ + message: t('reservoirPage.PullingArtifact', { + name: task.artifact.name, + version: task.version, + }), + type: 'info', + open: true, + duration: 0, + backgroundTask: { + status: 'pending', + taskId: task.taskId, + promise: null, + percent: 0, + onChange: { + resolved: (_data, _notification) => { + return { + type: 'success', + message: t('reservoirPage.SuccessFullyPulledArtifact', { + name: task.artifact.name, + version: task.version, + }), + toText: t('reservoirPage.GoToArtifact'), + to: `/reservoir/${task.artifact.id}`, + }; + }, + rejected: (_data, _notification) => { + return t('reservoirPage.FailedToPullArtifact', { + name: task.artifact.name, + version: task.version, + }); + }, + }, + }, + }); + }); + }} + onCancel={() => { + setSelectedArtifact(null); + setSelectedRevision([]); + }} + /> + { + toggleOpenHuggingFaceModal(); + navigate(`/reservoir/${toLocalId(artifactId)}`); + }} + onCancel={toggleOpenHuggingFaceModal} + /> + setSelectedArtifacts([])} + onOk={() => { + updateFetchKey(); + setSelectedArtifacts([]); + setSelectedArtifactIdList([]); + }} + /> + setSelectedRestoreArtifacts([])} + onOk={() => { + updateFetchKey(); + setSelectedRestoreArtifacts([]); + setSelectedArtifactIdList([]); + }} + /> + + ); +}; + +export default ReservoirArtifactsList; diff --git a/react/src/pages/ReservoirPage.tsx b/react/src/pages/ReservoirPage.tsx index 0969b01c31..38f4cf02ab 100644 --- a/react/src/pages/ReservoirPage.tsx +++ b/react/src/pages/ReservoirPage.tsx @@ -1,204 +1,15 @@ -import { INITIAL_FETCH_KEY, useUpdatableState } from '../hooks'; -import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions'; -import { useToggle } from 'ahooks'; -import { - theme, - Col, - Row, - Statistic, - Card, - Button, - Tooltip, - Typography, -} from 'antd'; -import { - BAICard, - BAIFlex, - BAIArtifactTable, - BAIImportArtifactModal, - BAIGraphQLPropertyFilter, - BAIImportFromHuggingFaceModal, - BAIHuggingFaceIcon, - toLocalId, - BAIImportArtifactModalArtifactFragmentKey, - BAIImportArtifactModalArtifactRevisionFragmentKey, - BAIDeactivateArtifactsModalArtifactsFragmentKey, - BAIDeactivateArtifactsModal, - BAIActivateArtifactsModalArtifactsFragmentKey, - BAIActivateArtifactsModal, -} from 'backend.ai-ui'; -import _ from 'lodash'; -import { BanIcon, Brain, UndoIcon } from 'lucide-react'; -import React, { useMemo, useDeferredValue, useState } from 'react'; +import { theme } from 'antd'; +import { BAICard, BAIFlex } from 'backend.ai-ui'; import { useTranslation } from 'react-i18next'; -import { graphql, useLazyLoadQuery } from 'react-relay'; -import { useNavigate } from 'react-router-dom'; -import { - ReservoirPageQuery, - ReservoirPageQuery$data, - ReservoirPageQuery$variables, -} from 'src/__generated__/ReservoirPageQuery.graphql'; -import BAIFetchKeyButton from 'src/components/BAIFetchKeyButton'; -import BAIRadioGroup from 'src/components/BAIRadioGroup'; -import { useSetBAINotification } from 'src/hooks/useBAINotification'; -import { - withDefault, - JsonParam, - StringParam, - useQueryParams, -} from 'use-query-params'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import ReservoirArtifactsList from 'src/components/ReservoirArtifactsList'; -const getStatusFilter = (status: string) => { - return { availability: [status] }; -}; - -type ArtifactNode = NonNullable< - ReservoirPageQuery$data['artifacts'] ->['edges'][number]['node']; - -const ReservoirPage: React.FC = () => { +const ReservoirPage = () => { const { t } = useTranslation(); const { token } = theme.useToken(); const navigate = useNavigate(); - const { upsertNotification } = useSetBAINotification(); - - const [selectedArtifactIdList, setSelectedArtifactIdList] = useState< - { - id: string; - data: ArtifactNode; - }[] - >([]); - const [selectedArtifacts, setSelectedArtifacts] = - useState([]); - const [selectedRestoreArtifacts, setSelectedRestoreArtifacts] = - useState([]); - const [selectedArtifact, setSelectedArtifact] = - useState(null); - const [selectedRevision, setSelectedRevision] = - useState([]); - const [openHuggingFaceModal, { toggle: toggleOpenHuggingFaceModal }] = - useToggle(); - - const { - baiPaginationOption, - tablePaginationOption, - setTablePaginationOption, - } = useBAIPaginationOptionStateOnSearchParam({ - current: 1, - pageSize: 10, - }); - - const [queryParams, setQuery] = useQueryParams({ - filter: withDefault(JsonParam, {}), - mode: withDefault(StringParam, 'ALIVE'), - }); - const jsonStringFilter = JSON.stringify(queryParams.filter); - - const queryVariables: ReservoirPageQuery$variables = useMemo( - () => ({ - offset: baiPaginationOption.offset, - limit: baiPaginationOption.limit, - order: [ - { - field: 'UPDATED_AT', - direction: 'DESC', - }, - ], - filter: _.merge( - {}, - JSON.parse(jsonStringFilter || '{}'), - getStatusFilter(queryParams.mode), - ), - }), - [ - baiPaginationOption.offset, - baiPaginationOption.limit, - jsonStringFilter, - queryParams.mode, - ], - ); - const deferredQueryVariables = useDeferredValue(queryVariables); - - const [fetchKey, updateFetchKey] = useUpdatableState(INITIAL_FETCH_KEY); - const deferredFetchKey = useDeferredValue(fetchKey); - - // const [rescanArtifacts, isInflightRescanArtifacts] = - // useMutation(graphql` - // mutation ReservoirPageRescanArtifactsMutation( - // $input: ScanArtifactsInput! - // ) { - // scanArtifacts(input: $input) { - // artifacts { - // id - // } - // } - // } - // `); - - const queryRef = useLazyLoadQuery( - graphql` - query ReservoirPageQuery( - $order: [ArtifactOrderBy!] - $limit: Int! - $offset: Int! - $filter: ArtifactFilter! - ) { - defaultArtifactRegistry(artifactType: MODEL) { - name - type - } - total: artifacts( - limit: 0 - offset: 0 - filter: { availability: [ALIVE, DELETED] } - ) { - count - } - artifacts( - orderBy: $order - limit: $limit - offset: $offset - filter: $filter - ) { - count - edges { - node { - id - ...BAIArtifactTableArtifactFragment - ...BAIImportArtifactModalArtifactFragment - ...BAIDeactivateArtifactsModalArtifactsFragment - ...BAIActivateArtifactsModalArtifactsFragment - revisions( - first: 1 - orderBy: { field: VERSION, direction: DESC } - ) { - edges { - node { - id - ...BAIImportArtifactModalArtifactRevisionFragment - } - } - } - } - } - } - } - `, - deferredQueryVariables, - { - fetchKey: - deferredFetchKey === INITIAL_FETCH_KEY ? undefined : deferredFetchKey, - fetchPolicy: - deferredFetchKey === INITIAL_FETCH_KEY - ? 'store-and-network' - : 'network-only', - }, - ); - - const { artifacts, defaultArtifactRegistry, total } = queryRef; - const isAvailableUsingHuggingFace = - defaultArtifactRegistry?.type === 'HUGGINGFACE'; - const mode = queryParams.mode; + const [searchParams] = useSearchParams(); + const currentTab = searchParams.get('tab') || 'artifacts'; // TODO: implement when reservoir supports other types // const typeFilterGenerator = (type: 'IMAGE' | 'PACKAGE' | 'MODEL') => { @@ -210,234 +21,31 @@ const ReservoirPage: React.FC = () => { return ( - - - - } - valueStyle={{ - color: token.colorPrimary, - }} - /> - - - + navigate({ + pathname: '/reservoir', + search: new URLSearchParams({ tab: key }).toString(), + }) + } > - - - - { - setQuery({ mode: e.target.value }, 'replaceIn'); - setTablePaginationOption({ current: 1 }); - setSelectedArtifactIdList([]); - setSelectedArtifacts([]); - setSelectedRestoreArtifacts([]); - }} - /> - { - setQuery({ filter: value ?? {} }, 'replaceIn'); - }} - value={queryParams.filter} - filterProperties={[ - { - fixedOperator: 'contains', - key: 'name', - propertyLabel: t('reservoirPage.Name'), - type: 'string', - }, - { - fixedOperator: 'contains', - key: 'source', - propertyLabel: t('reservoirPage.Source'), - type: 'string', - }, - { - fixedOperator: 'contains', - key: 'registry', - propertyLabel: t('reservoirPage.Registry'), - type: 'string', - }, - ]} - /> - - - {selectedArtifactIdList.length > 0 && ( - - - {t('general.NSelected', { - count: selectedArtifactIdList.length, - })} - - - - )} - - - edge.node)} - onClickPull={(artifactId: string, revisionId: string) => { - artifacts.edges.forEach((artifact) => { - if (artifact.node.id === artifactId) { - setSelectedArtifact(artifact.node); - artifact.node.revisions.edges.forEach((revision) => { - if (revision.node.id === revisionId) { - setSelectedRevision([revision.node]); - return; - } - }); - return; - } - }); - }} - onClickDelete={(artifactId: string) => { - artifacts.edges.forEach((edge) => { - if (edge.node.id === artifactId) { - setSelectedArtifacts([edge.node]); - return; - } - }); - }} - onClickRestore={(artifactId: string) => { - artifacts.edges.forEach((edge) => { - if (edge.node.id === artifactId) { - setSelectedRestoreArtifacts([edge.node]); - return; - } - }); - }} - rowSelection={{ - type: 'checkbox', - onChange: (keys) => { - const artifactIdList = artifacts.edges.map((e) => e.node.id); - setSelectedArtifactIdList((prev) => { - const _filtered = prev.filter( - (v) => !artifactIdList.includes(v.id), - ); - const _selected = artifacts.edges - .filter((e) => keys.includes(e.node.id)) - .map((arr) => ({ - id: arr.node.id, - data: arr.node, - })); - return _filtered.concat(_selected); - }); - }, - selectedRowKeys: selectedArtifactIdList.map((v) => v.id), - }} - loading={deferredQueryVariables !== queryVariables} - pagination={{ - pageSize: tablePaginationOption.pageSize, - current: tablePaginationOption.current, - total: artifacts.count, - onChange: (current, pageSize) => { - if (_.isNumber(current) && _.isNumber(pageSize)) { - setTablePaginationOption({ current, pageSize }); - } - }, - }} - /> - + {currentTab === 'artifacts' && } {/* TODO: implement audit log for reservoir page */} {/* {curTabKey === 'audit' ? ( }> @@ -471,83 +79,6 @@ const ReservoirPage: React.FC = () => { ) : null} */} - { - setSelectedArtifact(null); - setSelectedRevision([]); - tasks.forEach((task) => { - upsertNotification({ - message: t('reservoirPage.PullingArtifact', { - name: task.artifact.name, - version: task.version, - }), - type: 'info', - open: true, - duration: 0, - backgroundTask: { - status: 'pending', - taskId: task.taskId, - promise: null, - percent: 0, - onChange: { - resolved: (_data, _notification) => { - return { - type: 'success', - message: t('reservoirPage.SuccessFullyPulledArtifact', { - name: task.artifact.name, - version: task.version, - }), - toText: t('reservoirPage.GoToArtifact'), - to: `/reservoir/${task.artifact.id}`, - }; - }, - rejected: (_data, _notification) => { - return t('reservoirPage.FailedToPullArtifact', { - name: task.artifact.name, - version: task.version, - }); - }, - }, - }, - }); - }); - }} - onCancel={() => { - setSelectedArtifact(null); - setSelectedRevision([]); - }} - /> - { - toggleOpenHuggingFaceModal(); - navigate(`/reservoir/${toLocalId(artifactId)}`); - }} - onCancel={toggleOpenHuggingFaceModal} - /> - setSelectedArtifacts([])} - onOk={() => { - updateFetchKey(); - setSelectedArtifacts([]); - setSelectedArtifactIdList([]); - }} - /> - setSelectedRestoreArtifacts([])} - onOk={() => { - updateFetchKey(); - setSelectedRestoreArtifacts([]); - setSelectedArtifactIdList([]); - }} - /> ); }; diff --git a/react/src/pages/UserCredentialsPage.tsx b/react/src/pages/UserCredentialsPage.tsx index c21b08639c..feca697f01 100644 --- a/react/src/pages/UserCredentialsPage.tsx +++ b/react/src/pages/UserCredentialsPage.tsx @@ -29,7 +29,7 @@ const UserCredentialsPage: React.FC = () => { onTabChange={(key) => navigate({ pathname: '/credential', - search: `?tab=${key}`, + search: new URLSearchParams({ tab: key }).toString(), }) } tabList={tabItems}