+
+
+
+ {artifact?.name}
+
+
+ {getTypeIcon(artifact?.type ?? '', 18)}
+ {artifact?.type ?? ''.toUpperCase()}
+
+
+ {
+ updateFetchKey();
+ }}
+ />
+
+
+ {pullingArtifacts.length > 0 && (
+
+ {pullingArtifacts.map((frgmt) => (
+
+ ))}
+
+ )}
+
+
}
+ onClick={() => {
+ if (!latestArtifact) return;
+ setSelectedRevision([latestArtifact]);
+ }}
+ disabled={!latestArtifact || latestArtifact.status !== 'SCANNED'}
+ >
+ {`Pull latest(${latestArtifact?.version}) version`}
+
+ }
+ style={{ marginBottom: token.marginMD }}
+ >
+
+
+ {artifact?.name}
+
+
+
+ {getTypeIcon(artifact?.type ?? '')}
+ {artifact?.type.toUpperCase()}
+
+
+
+
+ {/* {convertToDecimalUnit(latestArtifact?.size, 'auto')?.displayValue} */}
+
+
+
+ {artifact?.source ? (
+
+ {artifact.source.name || 'N/A'}
+
+ ) : (
+ 'N/A'
+ )}
+
+
+
+ {artifact?.registry
+ ? `${artifact.registry.name}(${artifact.registry.url})`
+ : 'N/A'}
+
+
+
+ {artifact?.updatedAt
+ ? dayjs(artifact?.updatedAt).format('lll')
+ : 'N/A'}
+
+
+
+ {artifact?.description || 'No description available'}
+
+
+
+
+
+
+
+
+ {
+ setQuery({ filter: value ?? {} }, 'replaceIn');
+ }}
+ filterProperties={[
+ {
+ fixedOperator: 'eq',
+ propertyLabel: 'Status',
+ key: 'status',
+ type: 'enum',
+ options: [
+ {
+ label: 'SCANNED',
+ value: 'SCANNED',
+ },
+ {
+ label: 'PULLING',
+ value: 'PULLING',
+ },
+ {
+ label: 'PULLED',
+ value: 'PULLED',
+ },
+ {
+ label: 'VERIFYING',
+ value: 'VERIFYING',
+ },
+ {
+ label: 'NEEDS_APPROVAL',
+ value: 'NEEDS_APPROVAL',
+ },
+ {
+ label: 'FAILED',
+ value: 'FAILED',
+ },
+ {
+ label: 'AVAILABLE',
+ value: 'AVAILABLE',
+ },
+ {
+ label: 'REJECTED',
+ value: 'REJECTED',
+ },
+ ],
+ },
+ {
+ fixedOperator: 'contains',
+ propertyLabel: 'Version',
+ key: 'version',
+ type: 'string',
+ },
+ {
+ propertyLabel: 'Artifact ID',
+ key: 'artifactId',
+ valueMode: 'scalar',
+ type: 'string',
+ },
+ ]}
+ />
+ {selectedRevisionIdList.length > 0 ? (
+
+ {selectedRevisionIdList.length} selected
+
+ }
+ />
+
+
+ }
+ onClick={() => {
+ if (!artifact) return;
+ setSelectedRevisions(
+ selectedRevisionIdList.flatMap((arr) => arr.data),
+ );
+ }}
+ />
+
+
+ ) : null}
+
+ e.node),
+ )}
+ latestRevisionFrgmt={artifact?.latestVersion.edges[0].node}
+ loading={deferredQueryVariables !== queryVariables}
+ onClickDownload={(revisionId: string) => {
+ artifact?.revisions.edges.forEach((edge) => {
+ if (edge.node.id === revisionId) {
+ return setSelectedRevision([edge.node]);
+ }
+ });
+ }}
+ pagination={{
+ current: tablePaginationOption.current,
+ pageSize: tablePaginationOption.pageSize,
+ total: artifact?.revisions.count ?? 0,
+ onChange: (page, pageSize) => {
+ if (_.isNumber(page) && _.isNumber(pageSize)) {
+ setTablePaginationOption({
+ current: page,
+ pageSize: pageSize,
+ });
+ }
+ },
+ }}
+ onRow={(record) => ({
+ onClick: (event) => {
+ event.stopPropagation();
+ const target = event.target as HTMLElement;
+ // skip when clicking buttons or links inside the row
+ if (target.closest('button') || target.closest('a')) {
+ return;
+ }
+ if (!artifact) return;
+ const selectedNode = artifact.revisions.edges.find(
+ (e) => e.node.id === record.id,
+ )?.node;
+ if (!selectedNode) return;
+ setSelectedRevisionIdList((prev) => {
+ const _filtered = prev.filter((v) => v.id !== record.id);
+ if (_filtered.length === prev.length) {
+ return [...prev, { id: record.id, data: [selectedNode] }];
+ } else {
+ return _filtered;
+ }
+ });
+ },
+ })}
+ rowSelection={{
+ type: 'checkbox',
+ onChange: (keys) => {
+ if (!artifact) return;
+ const revisions = artifact.revisions;
+ const revisionsIds = revisions.edges.map((e) => e.node.id);
+ setSelectedRevisionIdList((prev) => {
+ const _filtered = prev.filter(
+ (v) => !revisionsIds.includes(v.id),
+ );
+ const _selected = revisions.edges
+ .filter((e) => keys.includes(e.node.id))
+ .map((arr) => ({
+ id: arr.node.id,
+ data: [arr.node],
+ }));
+ return _filtered.concat(_selected);
+ });
+ },
+ selectedRowKeys: selectedRevisionIdList.map((arr) => arr.id),
+ }}
+ />
+
+
+
+ {/* {artifact.dependencies && artifact.dependencies.length > 0 && (
+
+
+ {artifact.dependencies.map((dep) => (
+
+ {dep}
+
+ ))}
+
+
+ )}
+
+ {artifact.tags && artifact.tags.length > 0 && (
+
+
+ {artifact.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ )} */}
+
{
+ setSelectedRevisions([]);
+ }}
+ onCancel={() => {
+ setSelectedRevisions([]);
+ }}
+ />
+ {
+ setSelectedRevision([]);
+ }}
+ onCancel={() => {
+ setSelectedRevision([]);
+ }}
+ />
+
+ );
+};
+
+export default ReservoirArtifactDetailPage;
diff --git a/react/src/pages/ReservoirPage.tsx b/react/src/pages/ReservoirPage.tsx
index ed4af1898e..a0f9e5f4e0 100644
--- a/react/src/pages/ReservoirPage.tsx
+++ b/react/src/pages/ReservoirPage.tsx
@@ -1,51 +1,42 @@
-import BAIRadioGroup from '../components/BAIRadioGroup';
-import ReservoirArtifactDetail from '../components/ReservoirArtifactDetail';
-import ReservoirArtifactList from '../components/ReservoirArtifactList';
-import ReservoirAuditLogList from '../components/ReservoirAuditLogList';
-import { handleRowSelectionChange } from '../helper';
-import { useUpdatableState } from '../hooks';
+import { INITIAL_FETCH_KEY, useUpdatableState } from '../hooks';
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
import { useDeferredQueryParams } from '../hooks/useDeferredQueryParams';
-import type { ReservoirArtifact, ReservoirAuditLog } from '../types/reservoir';
+import { theme, Col, Row, Statistic, Card } from 'antd';
import {
- Button,
- Badge,
- Typography,
- theme,
- Col,
- Row,
- Tooltip,
- Statistic,
- Card,
- Skeleton,
-} from 'antd';
-import { BAICard, BAIFlex, BAIPropertyFilter } from 'backend.ai-ui';
+ BAICard,
+ BAIFlex,
+ BAIArtifactTable,
+ BAIImportArtifactModal,
+ BAIGraphQLPropertyFilter,
+ toLocalId,
+ BAIImportArtifactModalArtifactRevisionFragmentKey,
+ BAIImportArtifactModalArtifactFragmentKey,
+} from 'backend.ai-ui';
import _ from 'lodash';
-import {
- Trash2,
- CheckCircle,
- HardDrive,
- Activity,
- Calendar,
- DatabaseIcon,
-} from 'lucide-react';
-import React, { useState, useMemo, useRef, Suspense } from 'react';
+import { Package, Brain, Container } from 'lucide-react';
+import React, { useMemo, useDeferredValue, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useParams } from 'react-router-dom';
-import { StringParam, withDefault, useQueryParam } from 'use-query-params';
-
-type TabKey = 'artifacts' | 'audit';
+import { graphql, useLazyLoadQuery } from 'react-relay';
+import { useNavigate } from 'react-router-dom';
+import {
+ ReservoirPageQuery,
+ ReservoirPageQuery$variables,
+} from 'src/__generated__/ReservoirPageQuery.graphql';
+import BAIFetchKeyButton from 'src/components/BAIFetchKeyButton';
+import { withDefault, JsonParam } from 'use-query-params';
const ReservoirPage: React.FC = () => {
const { t } = useTranslation();
const { token } = theme.useToken();
- const { artifactId } = useParams<{ artifactId: string }>();
- const [selectedArtifactList, setSelectedArtifactList] = useState<
- Array