diff --git a/react/src/App.tsx b/react/src/App.tsx
index e12681a1c..4bd66d7df 100644
--- a/react/src/App.tsx
+++ b/react/src/App.tsx
@@ -11,7 +11,7 @@ import { useSuspendedBackendaiClient } from './hooks';
import { useBAISettingUserState } from './hooks/useBAISetting';
import Page401 from './pages/Page401';
import Page404 from './pages/Page404';
-import VFolderListPage from './pages/VFolderListPage';
+// import VFolderListPage from './pages/VFolderListPage';
import { Skeleton, theme } from 'antd';
import React, { Suspense } from 'react';
import { FC } from 'react';
@@ -64,6 +64,10 @@ const AdminDashboardPage = React.lazy(
);
const DashboardPage = React.lazy(() => import('./pages/DashboardPage'));
+const NeoVFolderListPage = React.lazy(
+ () => import('./pages/NeoVFolderListPage'),
+);
+
const RedirectToStart = () => {
useSuspendedBackendaiClient();
const pathName = '/start';
@@ -250,7 +254,8 @@ const router = createBrowserRouter([
handle: { labelKey: 'webui.menu.Data&Storage' },
element: (
-
+ {/* */}
+
),
},
diff --git a/react/src/components/AllocatedResourcesCard.tsx b/react/src/components/AllocatedResourcesCard.tsx
index 853a44a91..7284a31d5 100644
--- a/react/src/components/AllocatedResourcesCard.tsx
+++ b/react/src/components/AllocatedResourcesCard.tsx
@@ -1,8 +1,21 @@
-import { useCurrentProjectValue } from '../hooks/useCurrentProject';
+import { humanReadableBinarySize, iSizeToSize } from '../helper';
+import { useSuspendedBackendaiClient } from '../hooks';
+import { useResourceSlotsDetails } from '../hooks/backendai';
+import { useSuspenseTanQuery } from '../hooks/reactQueryAlias';
+import {
+ useCurrentProjectValue,
+ useCurrentResourceGroupValue,
+} from '../hooks/useCurrentProject';
+import {
+ ResourceSlots,
+ ResourceAllocation,
+ limitParser,
+} from '../hooks/useResourceLimitAndRemaining';
+import BAILayoutCard from './BAILayoutCard';
import Flex from './Flex';
import ResourceGroupSelect from './ResourceGroupSelect';
+import ResourceGroupSelectForCurrentProject from './ResourceGroupSelectForCurrentProject';
import ResourceUnit, { ResourceUnitProps } from './ResourceUnit';
-import BAILayoutCard from './BAILayoutCard';
import { QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
import {
Button,
@@ -30,33 +43,101 @@ const AllocatedResourcesCard: React.FC = ({
}) => {
const { token } = theme.useToken();
const { t } = useTranslation();
+ const currentResourceGroup = useCurrentResourceGroupValue(); // use global state
const currentProject = useCurrentProjectValue();
+ const baiClient = useSuspendedBackendaiClient();
+ const { mergedResourceSlots } = useResourceSlotsDetails();
+
+ const { data: resourceAllocation, refetch } = useSuspenseTanQuery<
+ ResourceAllocation | undefined
+ >({
+ queryKey: ['check-presets', currentProject.name, currentResourceGroup],
+ queryFn: () => {
+ if (currentResourceGroup) {
+ return baiClient.resourcePreset
+ .check({
+ group: currentProject.name,
+ scaling_group: currentResourceGroup,
+ })
+ .catch(() => {});
+ } else {
+ return;
+ }
+ },
+ // onSuccess: (data) => {
+ // if (!data) {
+ // refetch();
+ // }
+ // },
+ // suspense: !_.isEmpty(currentResourceGroup), //prevent flicking
+ });
+
+ const mergeResources = (remaining: any, using: any) => {
+ let merged: ResourceSlots = {
+ cpu: '',
+ mem: '',
+ };
+
+ [remaining, using].forEach((obj) => {
+ Object.entries(obj).forEach(([key, value]) => {
+ const numValue = parseFloat(value as string) || 0;
+ merged[key] = (parseFloat(merged[key] || '0') + numValue).toString();
+ });
+ });
+
+ return merged;
+ };
+
+ const remaining =
+ resourceAllocation?.scaling_groups[currentResourceGroup as string]
+ ?.remaining || {};
+ const using =
+ resourceAllocation?.scaling_groups[currentResourceGroup as string]?.using ||
+ {};
+ const mergedResources: ResourceSlots = mergeResources(remaining, using);
+
+ const accelerators = _.omit(mergedResources, ['cpu', 'mem']);
+ const usingAccelerators: { [key: string]: string } = _.omit(using, [
+ 'cpu',
+ 'mem',
+ ]);
+
+ console.log(iSizeToSize(limitParser(using?.mem) + '', 'g', 0));
+
+ const acceleratorData = _.map(accelerators, (value, key) => ({
+ name: mergedResourceSlots[key]?.human_readable_name || key.toUpperCase(),
+ displayUnit: mergedResourceSlots[key]?.display_unit || 'Unit',
+ value: usingAccelerators[key],
+ percentage:
+ (parseInt(usingAccelerators[key]) / parseInt(mergedResources[key])) * 100,
+ }));
- const resourceUnitMockData: Array = [
+ const resourceUnitData: Array = [
{
name: 'CPU',
displayUnit: 'Core',
- value: 12,
- percentage: (4 / 12) * 100,
+ value: using?.cpu as string,
+ percentage:
+ (parseInt(using?.cpu as string) / parseInt(mergedResources.cpu)) * 100,
},
{
name: 'RAM',
displayUnit: 'GiB',
- value: 256,
- percentage: (8 / 12) * 100,
- },
- {
- name: 'FGPU',
- displayUnit: 'GiB',
- value: 3.5,
- percentage: (4 / 12) * 100,
- },
- {
- name: 'ATOM',
- displayUnit: 'Unit',
- value: 2,
- percentage: (2 / 12) * 100,
+ value: iSizeToSize(limitParser(using?.mem) + '', 'g', 0)?.number?.toFixed(
+ 1,
+ ) as string,
+ percentage:
+ (parseInt(
+ iSizeToSize(limitParser(using?.mem) + '', 'g', 0)
+ ?.numberFixed as string,
+ ) /
+ parseInt(
+ iSizeToSize(limitParser(mergedResources.mem) + '', 'g', 0)
+ ?.numberFixed as string,
+ )) *
+ 100,
},
+ ...acceleratorData,
];
return (
= ({
}
extra={
<>
-
+ variant="borderless"
+ showSearch
+ />
}
style={{ color: 'inherit' }}
+ onClick={() => {
+ refetch();
+ }}
/>
>
}
style={{ width: width ?? 678, height: 192 }}
>
-
- {_.map(
- resourceUnitMockData,
- (resourceUnit: ResourceUnitProps, index) => (
- <>
-
+ {_.map(resourceUnitData, (resourceUnit: ResourceUnitProps, index) => (
+
+
+ {index < resourceUnitData.length - 1 && (
+
- {index < resourceUnitMockData.length - 1 && (
-
- )}
- >
- ),
- )}
+ )}
+
+ ))}
{/* = ({
fontSize: token.fontSizeSM,
color: '#333333',
textAlign: 'center',
+ whiteSpace: 'pre-line',
}}
>
{title}
diff --git a/react/src/components/MainLayout/WebUISider.tsx b/react/src/components/MainLayout/WebUISider.tsx
index 6a1097412..7c250dbbf 100644
--- a/react/src/components/MainLayout/WebUISider.tsx
+++ b/react/src/components/MainLayout/WebUISider.tsx
@@ -79,7 +79,6 @@ const WebUISider: React.FC = (props) => {
baiClient?.supports('user-committed-image') ?? false;
const menuInPreparation = [
- 'data',
'my-environment',
'examples',
'serving',
@@ -116,15 +115,7 @@ const WebUISider: React.FC = (props) => {
type: 'group',
children: [
{
- label: (
-
- {t('webui.menu.Data&Storage')}
-
- ),
+ label: t('webui.menu.Data&Storage'),
icon: ,
key: 'data',
},
diff --git a/react/src/components/ResourceUnit.tsx b/react/src/components/ResourceUnit.tsx
index 77769aa5b..38b5d9d0f 100644
--- a/react/src/components/ResourceUnit.tsx
+++ b/react/src/components/ResourceUnit.tsx
@@ -5,9 +5,10 @@ import React from 'react';
export interface ResourceUnitProps {
name: string;
displayUnit: string;
- value: number;
+ value: string;
percentage: number;
color?: string;
+ style?: React.CSSProperties;
}
const ResourceUnit: React.FC = ({
@@ -16,20 +17,11 @@ const ResourceUnit: React.FC = ({
value,
percentage,
color,
+ style,
}) => {
const { token } = theme.useToken();
return (
-
+
= ({
{name}
-
+
{
+export const limitParser = (limit: string | undefined) => {
if (limit === undefined) {
return undefined;
} else if (limit === 'Infinity') {
diff --git a/react/src/pages/AdminDashboardPage.tsx b/react/src/pages/AdminDashboardPage.tsx
index 516974a0c..6197845c1 100644
--- a/react/src/pages/AdminDashboardPage.tsx
+++ b/react/src/pages/AdminDashboardPage.tsx
@@ -208,25 +208,25 @@ const AdminDashboardPage: React.FC = (props) => {
{
name: 'CPU',
displayUnit: 'Core',
- value: 12,
+ value: '12',
percentage: (4 / 12) * 100,
},
{
name: 'RAM',
displayUnit: 'GiB',
- value: 256,
+ value: '256',
percentage: (8 / 12) * 100,
},
{
name: 'FGPU',
displayUnit: 'GiB',
- value: 3.5,
+ value: '3.5',
percentage: (4 / 12) * 100,
},
{
name: 'ATOM',
displayUnit: 'Unit',
- value: 2,
+ value: '2',
percentage: (2 / 12) * 100,
},
];
diff --git a/react/src/pages/DashboardPage.tsx b/react/src/pages/DashboardPage.tsx
index 48a8f8e9d..75d4ffde9 100644
--- a/react/src/pages/DashboardPage.tsx
+++ b/react/src/pages/DashboardPage.tsx
@@ -8,10 +8,18 @@ import {
iSizeToSize,
localeCompare,
} from '../helper';
-import { useUpdatableState } from '../hooks';
+import {
+ useCurrentDomainValue,
+ useSuspendedBackendaiClient,
+ useUpdatableState,
+} from '../hooks';
+import { useCurrentKeyPairResourcePolicyLazyLoadQuery } from '../hooks/hooksUsingRelay';
import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions';
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
import { DashboardPageSessionListQuery } from './__generated__/DashboardPageSessionListQuery.graphql';
+import { DashboardPage_EndpointListQuery } from './__generated__/DashboardPage_EndpointListQuery.graphql';
+import { DashboardPage_UserInfoQuery } from './__generated__/DashboardPage_UserInfoQuery.graphql';
+import { DashboardPage_UserResourcePolicyQuery } from './__generated__/DashboardPage_UserResourcePolicyQuery.graphql';
import { QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
import {
Button,
@@ -30,7 +38,7 @@ import { ColumnType, TableRowSelection } from 'antd/lib/table/interface';
import graphql from 'babel-plugin-relay/macro';
import dayjs from 'dayjs';
import _ from 'lodash';
-import React, { useTransition } from 'react';
+import React, { useDeferredValue, useTransition } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLazyLoadQuery } from 'react-relay';
@@ -55,7 +63,7 @@ export const statusTagColor = {
const DashboardPage: React.FC = (props) => {
const { token } = theme.useToken();
const { t } = useTranslation();
-
+ const baiClient = useSuspendedBackendaiClient();
const [
sessionFetchKey,
// setSessionFetchKey
@@ -69,6 +77,41 @@ const DashboardPage: React.FC = (props) => {
pageSize: 10,
});
const { id: projectId } = useCurrentProjectValue();
+ const [servicesFetchKey, updateServicesFetchKey] =
+ useUpdatableState('initial-fetch');
+
+ const [{ keypair, keypairResourcePolicy }] =
+ useCurrentKeyPairResourcePolicyLazyLoadQuery();
+
+ const { user } = useLazyLoadQuery(
+ graphql`
+ query DashboardPage_UserInfoQuery($domain_name: String, $email: String) {
+ user(domain_name: $domain_name, email: $email) {
+ id
+ # https://github.com/lablup/backend.ai/pull/1354
+ resource_policy @since(version: "23.09.0")
+ }
+ }
+ `,
+ {
+ domain_name: useCurrentDomainValue(),
+ email: baiClient?.email,
+ },
+ );
+
+ const { user_resource_policy } =
+ useLazyLoadQuery(
+ graphql`
+ query DashboardPage_UserResourcePolicyQuery($user_RP_name: String) {
+ user_resource_policy(name: $user_RP_name) @since(version: "23.09.6") {
+ max_session_count_per_model_session
+ }
+ }
+ `,
+ {
+ user_RP_name: user?.resource_policy,
+ },
+ );
// TODO: refactor with useControllableState
const [selectedRowKeys, setSelectedRowKeys] = useState>([]);
@@ -80,7 +123,13 @@ const DashboardPage: React.FC = (props) => {
onChange: onSelectChange,
};
const [order, setOrder] = useState();
-
+ const [paginationState, setPaginationState] = useState<{
+ current: number;
+ pageSize: number;
+ }>({
+ current: 1,
+ pageSize: 10,
+ });
const { compute_session_list } =
useLazyLoadQuery(
graphql`
@@ -104,6 +153,10 @@ const DashboardPage: React.FC = (props) => {
session_id
name
created_at
+ containers {
+ live_stat
+ last_stat
+ }
terminated_at
status
occupied_slots
@@ -128,6 +181,37 @@ const DashboardPage: React.FC = (props) => {
},
);
+ const { endpoint_list: modelServiceList } =
+ useLazyLoadQuery(
+ graphql`
+ query DashboardPage_EndpointListQuery(
+ $offset: Int!
+ $projectID: UUID
+ $limit: Int!
+ $filter: String
+ ) {
+ endpoint_list(
+ offset: $offset
+ limit: $limit
+ project: $projectID
+ filter: $filter
+ ) {
+ total_count
+ }
+ }
+ `,
+ {
+ offset: baiPaginationOption.offset,
+ limit: 10,
+ projectID: projectId,
+ filter: null,
+ },
+ {
+ fetchPolicy: 'network-only',
+ fetchKey: servicesFetchKey,
+ },
+ );
+
const columns = filterEmptyItem>([
{
title: '#',
@@ -165,29 +249,119 @@ const DashboardPage: React.FC = (props) => {
},
{
title: 'Utils.',
- render: () => (
-
-
-
-
-
- ),
+ dataIndex: 'containers',
+ render: (value) => {
+ const aggregatedLiveStat: {
+ [key: string]: { capacity: number; current: number; ratio: number };
+ } = {
+ cpu_util: { capacity: 0, current: 0, ratio: 0 },
+ mem: { capacity: 0, current: 0, ratio: 0 },
+ };
+
+ value.forEach((container: { live_stat: any }) => {
+ const parsedLiveStat = _.isEmpty(container.live_stat)
+ ? null
+ : JSON.parse(container.live_stat);
+
+ if (parsedLiveStat) {
+ Object.keys(parsedLiveStat).forEach((statKey) => {
+ if (
+ statKey === 'cpu_util' ||
+ statKey === 'cpu_used' ||
+ statKey === 'mem' ||
+ statKey === 'io_read' ||
+ statKey === 'io_write' ||
+ statKey === 'io_scratch_size' ||
+ statKey === 'net_rx' ||
+ statKey === 'net_tx'
+ ) {
+ if (!aggregatedLiveStat[statKey]) {
+ aggregatedLiveStat[statKey] = {
+ capacity: 0,
+ current: 0,
+ ratio: 0,
+ };
+ }
+ aggregatedLiveStat[statKey].current += parseFloat(
+ parsedLiveStat[statKey].current,
+ );
+ aggregatedLiveStat[statKey].capacity += parseFloat(
+ parsedLiveStat[statKey].capacity,
+ );
+ return;
+ }
+ if (statKey.includes('_util') || statKey.includes('_mem')) {
+ if (!aggregatedLiveStat[statKey]) {
+ aggregatedLiveStat[statKey] = {
+ capacity: 0,
+ current: 0,
+ ratio: 0,
+ };
+ }
+ aggregatedLiveStat[statKey].current += parseFloat(
+ parsedLiveStat[statKey].current,
+ );
+ aggregatedLiveStat[statKey].capacity += parseFloat(
+ parsedLiveStat[statKey].capacity,
+ );
+ }
+ });
+ }
+ });
+
+ // Calculate utilization ratios
+ if (aggregatedLiveStat.cpu_util) {
+ aggregatedLiveStat.cpu_util.ratio =
+ aggregatedLiveStat.cpu_util.current /
+ aggregatedLiveStat.cpu_util.capacity || 0;
+ }
+ if (aggregatedLiveStat.mem) {
+ aggregatedLiveStat.mem.ratio =
+ aggregatedLiveStat.mem.current / aggregatedLiveStat.mem.capacity ||
+ 0;
+ }
+
+ Object.keys(aggregatedLiveStat).forEach((statKey) => {
+ if (statKey === 'cpu_util' || statKey === 'mem') return;
+ if (
+ statKey.indexOf('_util') !== -1 &&
+ aggregatedLiveStat[statKey].capacity > 0
+ ) {
+ aggregatedLiveStat[statKey].ratio =
+ aggregatedLiveStat[statKey].current / 100 || 0;
+ }
+ if (
+ statKey.indexOf('_mem') !== -1 &&
+ aggregatedLiveStat[statKey].capacity > 0
+ ) {
+ aggregatedLiveStat[statKey].ratio =
+ aggregatedLiveStat[statKey].current /
+ aggregatedLiveStat[statKey].capacity || 0;
+ }
+ });
+
+ return (
+
+ {Object.entries(
+ _.pickBy(
+ aggregatedLiveStat,
+ (_value: any, key: string) =>
+ key === 'cpu_util' || key === 'mem' || key.includes('_util'),
+ ),
+ ).map(([key, value]) => {
+ return (
+
+ );
+ })}
+
+ );
+ },
},
{
title: t('session.launcher.AIAccelerator'),
@@ -294,12 +468,17 @@ const DashboardPage: React.FC = (props) => {
level={1}
style={{ margin: 0, color: token.colorLinkHover }}
>
- 3
+ {
+ _.filter(
+ compute_session_list?.items,
+ (item) => item?.status !== 'TERMINATED',
+ ).length
+ }
- {' / 5'}
+ {`/ ${keypairResourcePolicy.max_concurrent_sessions}`}
}
@@ -315,7 +494,7 @@ const DashboardPage: React.FC = (props) => {
- {'hello world'}
+ {'blank'}
}
@@ -325,12 +504,12 @@ const DashboardPage: React.FC = (props) => {
level={1}
style={{ margin: 0, color: token.colorLinkHover }}
>
- 2
+ {modelServiceList?.total_count ?? 0}
- {' / 3'}
+ {`/ ${user_resource_policy?.max_session_count_per_model_session}`}
}
@@ -346,7 +525,7 @@ const DashboardPage: React.FC = (props) => {
- {'hello world'}
+ {'blank'}
}
@@ -356,12 +535,17 @@ const DashboardPage: React.FC = (props) => {
level={1}
style={{ margin: 0, color: token.colorLinkHover }}
>
- 1
+ {
+ _.filter(
+ compute_session_list?.items,
+ (item) => item?.type === 'SYSTEM',
+ ).length
+ }
- {' / 1'}
+ {` / ${keypairResourcePolicy?.max_concurrent_sftp_sessions}`}
}
@@ -386,12 +570,18 @@ const DashboardPage: React.FC = (props) => {
type="text"
icon={}
style={{ color: 'inherit' }}
+ onClick={() => {}}
/>
}
style={{ width: 1146, height: 360 }}
>
item?.status !== 'TERMINATED',
+ ),
+ )}
columns={columns}
>
@@ -416,23 +606,29 @@ const DashboardPage: React.FC = (props) => {
label: 'NVIDIA CUDA FGPU',
value: 'cuda.shares',
},
- {
- label: 'Rebellions ATOM',
- value: 'atom.device',
- },
+ // {
+ // label: 'Rebellions ATOM',
+ // value: 'atom.device',
+ // },
]}
>
}
style={{ color: 'inherit' }}
+ onClick={() => {}}
/>
}
style={{ width: 1146, height: 360 }}
>
item?.status !== 'TERMINATED',
+ ),
+ )}
columns={columns}
>
diff --git a/react/src/pages/NeoStorageQuotaCard.tsx b/react/src/pages/NeoStorageQuotaCard.tsx
new file mode 100644
index 000000000..341ade276
--- /dev/null
+++ b/react/src/pages/NeoStorageQuotaCard.tsx
@@ -0,0 +1,88 @@
+import BAILayoutCard from '../components/BAILayoutCard';
+import Flex from '../components/Flex';
+import StorageSelect from '../components/StorageSelect';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { Button, Divider, Typography, theme } from 'antd';
+import { CardProps } from 'antd/lib';
+
+interface NeoStorageQuotaCardItemProps {
+ type: 'project' | 'user';
+ name: string;
+ current: string | number;
+ total: string | number;
+}
+
+const NeoStorageQuotaCardItem: React.FC = ({
+ type,
+ name,
+ current,
+ total,
+}) => {
+ const { token } = theme.useToken();
+
+ return (
+
+
+
+
+ {type}
+
+
+ {name}
+
+
+
+ 50%
+
+
+
+ );
+};
+
+interface NeoStorageQuotaCardProps extends CardProps {}
+
+const NeoStorageQuotaCard: React.FC = ({
+ ...cardProps
+}) => {
+ const { token } = theme.useToken();
+
+ return (
+
+
+
+ {'Allocated Resources'}
+
+ } />
+
+
+
+ }
+ {...cardProps}
+ >
+
+
+
+
+
+
+ );
+};
+
+export default NeoStorageQuotaCard;
diff --git a/react/src/pages/NeoStorageStatusCard.tsx b/react/src/pages/NeoStorageStatusCard.tsx
new file mode 100644
index 000000000..ffb7fb9d8
--- /dev/null
+++ b/react/src/pages/NeoStorageStatusCard.tsx
@@ -0,0 +1,83 @@
+import BAICard from '../BAICard';
+import BAILayoutCard from '../components/BAILayoutCard';
+import Flex from '../components/Flex';
+import { Col, Divider, Row, Typography, theme } from 'antd';
+import { CardProps } from 'antd/lib';
+
+interface CardItemProps {
+ title: string;
+ currentValue: string | number;
+ totalValue?: string | number;
+}
+const CardItem: React.FC = ({
+ title,
+ currentValue,
+ totalValue,
+}) => {
+ const { token } = theme.useToken();
+
+ return (
+
+
+ {title}
+
+
+
+ {currentValue}
+
+ {totalValue && / {totalValue}}
+
+
+ );
+};
+
+interface NeoStorageStatusCardProps extends CardProps {}
+
+const NeoStorageStatusCard: React.FC = ({
+ ...cardProps
+}) => {
+ const { token } = theme.useToken();
+ return (
+
+ {'Storage Status'}
+
+ }
+ {...cardProps}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default NeoStorageStatusCard;
diff --git a/react/src/pages/NeoVFolderListPage.tsx b/react/src/pages/NeoVFolderListPage.tsx
new file mode 100644
index 000000000..3573ee1b8
--- /dev/null
+++ b/react/src/pages/NeoVFolderListPage.tsx
@@ -0,0 +1,38 @@
+import BAIStartSimpleCard from '../components/BAIStartSimpleCard';
+import Flex from '../components/Flex';
+import NeoStorageQuotaCard from './NeoStorageQuotaCard';
+import NeoStorageStatusCard from './NeoStorageStatusCard';
+import { FolderAddOutlined } from '@ant-design/icons';
+import { theme } from 'antd';
+
+const NeoVFolderListPage: React.FC = () => {
+ const { token } = theme.useToken();
+ return (
+
+
+ }
+ title={`Create Folder and \nUpload Files`}
+ bordered={false}
+ footerButtonProps={{
+ onClick: () => {
+ //TODO: 링크 연결
+ console.log('!@#!@#');
+ },
+ children: 'Create Folder',
+ }}
+ styles={{
+ body: {
+ height: 192,
+ width: 194,
+ },
+ }}
+ />
+
+
+
+
+ );
+};
+
+export default NeoVFolderListPage;