ajout app
This commit is contained in:
165
Voltaserve/ui/src/pages/workspace/workspace-files-page.tsx
Normal file
165
Voltaserve/ui/src/pages/workspace/workspace-files-page.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import cx from 'classnames'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import FileAPI from '@/client/api/file'
|
||||
import WorkspaceAPI from '@/client/api/workspace'
|
||||
import { swrConfig } from '@/client/options'
|
||||
import Path from '@/components/common/path'
|
||||
import FileCopy from '@/components/file/file-copy'
|
||||
import FileCreate from '@/components/file/file-create'
|
||||
import FileMove from '@/components/file/file-move'
|
||||
import FileRename from '@/components/file/file-rename'
|
||||
import FileToolbar from '@/components/file/file-toolbar'
|
||||
import FileDelete from '@/components/file/fle-idelete'
|
||||
import FileList from '@/components/file/list'
|
||||
import FileSharing from '@/components/file/sharing'
|
||||
import { decodeQuery } from '@/helpers/query'
|
||||
import { filePaginationSteps, filesPaginationStorage } from '@/infra/pagination'
|
||||
import {
|
||||
PagePagination,
|
||||
Spinner,
|
||||
usePageMonitor,
|
||||
usePagePagination,
|
||||
variables,
|
||||
} from '@/lib'
|
||||
import { listUpdated } from '@/store/entities/files'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hook'
|
||||
import { selectionUpdated } from '@/store/ui/files'
|
||||
|
||||
const WorkspaceFilesPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id, fileId } = useParams()
|
||||
const [searchParams] = useSearchParams()
|
||||
const query = decodeQuery(searchParams.get('q') as string)
|
||||
const dispatch = useAppDispatch()
|
||||
const sortBy = useAppSelector((state) => state.ui.files.sortBy)
|
||||
const sortOrder = useAppSelector((state) => state.ui.files.sortOrder)
|
||||
const iconScale = useAppSelector((state) => state.ui.files.iconScale)
|
||||
const { data: workspace } = WorkspaceAPI.useGetById(id, swrConfig())
|
||||
const { page, size, steps, setPage, setSize } = usePagePagination({
|
||||
navigate,
|
||||
location,
|
||||
storage: filesPaginationStorage(),
|
||||
steps: filePaginationSteps(),
|
||||
})
|
||||
const {
|
||||
data: list,
|
||||
error,
|
||||
isLoading,
|
||||
} = FileAPI.useList(
|
||||
fileId!,
|
||||
{
|
||||
size,
|
||||
page,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
query: query ? { text: query } : undefined,
|
||||
},
|
||||
swrConfig(),
|
||||
)
|
||||
const { hasPageSwitcher, hasSizeSelector } = usePageMonitor({
|
||||
totalElements: list?.totalElements || 0,
|
||||
totalPages: list?.totalPages || 1,
|
||||
steps,
|
||||
})
|
||||
const hasPagination = hasPageSwitcher || hasSizeSelector
|
||||
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
dispatch(listUpdated(list))
|
||||
}
|
||||
}, [list, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>{workspace && <title>{workspace.name}</title>}</Helmet>
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'w-full',
|
||||
'gap-2.5',
|
||||
'grow',
|
||||
'overflow-hidden',
|
||||
)}
|
||||
>
|
||||
{workspace && fileId ? (
|
||||
<Path
|
||||
rootId={workspace.rootId}
|
||||
fileId={fileId}
|
||||
maxCharacters={30}
|
||||
onClick={(fileId) => {
|
||||
dispatch(selectionUpdated([]))
|
||||
navigate(`/workspace/${workspace.id}/file/${fileId}`)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<FileToolbar list={list} />
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-1.5',
|
||||
'grow',
|
||||
'overflow-y-auto',
|
||||
'overflow-x-hidden',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'w-full',
|
||||
'overflow-y-auto',
|
||||
'overflow-x-hidden',
|
||||
'border-t',
|
||||
'border-t-gray-300',
|
||||
'dark:border-t-gray-600',
|
||||
{
|
||||
'border-b': hasPagination,
|
||||
'border-b-gray-300': hasPagination,
|
||||
'dark:border-b-gray-600': hasPagination,
|
||||
},
|
||||
'pt-1.5',
|
||||
'flex-grow',
|
||||
)}
|
||||
onClick={() => dispatch(selectionUpdated([]))}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'h-full',
|
||||
)}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : null}
|
||||
{list && !error ? <FileList list={list} scale={iconScale} /> : null}
|
||||
</div>
|
||||
{list ? (
|
||||
<PagePagination
|
||||
style={{ alignSelf: 'end', paddingBottom: variables.spacing }}
|
||||
totalElements={list.totalElements}
|
||||
totalPages={list.totalPages}
|
||||
page={page}
|
||||
size={size}
|
||||
steps={steps}
|
||||
setPage={setPage}
|
||||
setSize={setSize}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{list ? <FileSharing list={list} /> : null}
|
||||
<FileMove />
|
||||
<FileCopy />
|
||||
<FileCreate />
|
||||
<FileDelete />
|
||||
<FileRename />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceFilesPage
|
||||
51
Voltaserve/ui/src/pages/workspace/workspace-layout.tsx
Normal file
51
Voltaserve/ui/src/pages/workspace/workspace-layout.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { Heading, Tab, TabList, Tabs } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import WorkspaceAPI from '@/client/api/workspace'
|
||||
import { swrConfig } from '@/client/options'
|
||||
|
||||
const WorkspaceLayout = () => {
|
||||
const location = useLocation()
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data: workspace } = WorkspaceAPI.useGetById(id, swrConfig())
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const segments = location.pathname.split('/')
|
||||
const segment = segments[segments.length - 1]
|
||||
if (segment === 'settings') {
|
||||
setTabIndex(1)
|
||||
} else {
|
||||
setTabIndex(0)
|
||||
}
|
||||
}, [location])
|
||||
|
||||
if (!workspace) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-3.5', 'h-full')}>
|
||||
<Heading className={cx('text-heading')}>{workspace.name}</Heading>
|
||||
<Tabs variant="solid-rounded" colorScheme="gray" index={tabIndex}>
|
||||
<TabList>
|
||||
<Tab
|
||||
onClick={() =>
|
||||
navigate(`/workspace/${id}/file/${workspace.rootId}`)
|
||||
}
|
||||
>
|
||||
Files
|
||||
</Tab>
|
||||
<Tab onClick={() => navigate(`/workspace/${id}/settings`)}>
|
||||
Settings
|
||||
</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceLayout
|
||||
159
Voltaserve/ui/src/pages/workspace/workspace-list-page.tsx
Normal file
159
Voltaserve/ui/src/pages/workspace/workspace-list-page.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Link,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom'
|
||||
import {
|
||||
Heading,
|
||||
Link as ChakraLink,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
Avatar,
|
||||
Badge,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import WorkspaceAPI, { SortOrder } from '@/client/api/workspace'
|
||||
import { swrConfig } from '@/client/options'
|
||||
import { CreateWorkspaceButton } from '@/components/top-bar/top-bar-buttons'
|
||||
import prettyDate from '@/helpers/pretty-date'
|
||||
import { decodeQuery } from '@/helpers/query'
|
||||
import { workspacePaginationStorage } from '@/infra/pagination'
|
||||
import { SectionSpinner, PagePagination, usePagePagination } from '@/lib'
|
||||
|
||||
const WorkspaceListPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [searchParams] = useSearchParams()
|
||||
const query = decodeQuery(searchParams.get('q') as string)
|
||||
const { page, size, steps, setPage, setSize } = usePagePagination({
|
||||
navigate,
|
||||
location,
|
||||
storage: workspacePaginationStorage(),
|
||||
})
|
||||
const {
|
||||
data: list,
|
||||
error,
|
||||
mutate,
|
||||
} = WorkspaceAPI.useList(
|
||||
{ query, page, size, sortOrder: SortOrder.Desc },
|
||||
swrConfig(),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
mutate()
|
||||
}, [query, page, size, mutate])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Workspaces</title>
|
||||
</Helmet>
|
||||
<div className={cx('flex', 'flex-col', 'gap-3.5', 'pb-3.5')}>
|
||||
<Heading className={cx('pl-2', 'text-heading')}>Workspaces</Heading>
|
||||
{!list && error && (
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'h-[300px]',
|
||||
)}
|
||||
>
|
||||
<span>Failed to load workspaces.</span>
|
||||
</div>
|
||||
)}
|
||||
{!list && !error && <SectionSpinner />}
|
||||
{list && list.data.length === 0 && !error ? (
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'h-[300px]',
|
||||
)}
|
||||
>
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5', 'items-center')}>
|
||||
<span>There are no workspaces.</span>
|
||||
<CreateWorkspaceButton />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{list && list.data.length > 0 && (
|
||||
<Table variant="simple">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Organization</Th>
|
||||
<Th>Permission</Th>
|
||||
<Th>Date</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{list.data.map((w) => (
|
||||
<Tr key={w.id}>
|
||||
<Td>
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'gap-1.5',
|
||||
'items-center',
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
name={w.name}
|
||||
size="sm"
|
||||
className={cx('w-[40px]', 'h-[40px]')}
|
||||
/>
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
to={`/workspace/${w.id}/file/${w.rootId}`}
|
||||
className={cx('no-underline')}
|
||||
>
|
||||
<span>{w.name}</span>
|
||||
</ChakraLink>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
to={`/organization/${w.organization.id}/member`}
|
||||
className={cx('no-underline')}
|
||||
>
|
||||
{w.organization.name}
|
||||
</ChakraLink>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge>{w.permission}</Badge>
|
||||
</Td>
|
||||
<Td>{prettyDate(w.createTime)}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
{list && (
|
||||
<PagePagination
|
||||
style={{ alignSelf: 'end' }}
|
||||
totalElements={list.totalElements}
|
||||
totalPages={list.totalPages}
|
||||
page={page}
|
||||
size={size}
|
||||
steps={steps}
|
||||
setPage={setPage}
|
||||
setSize={setSize}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceListPage
|
||||
147
Voltaserve/ui/src/pages/workspace/workspace-settings-page.tsx
Normal file
147
Voltaserve/ui/src/pages/workspace/workspace-settings-page.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import {
|
||||
Divider,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
Progress,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { geEditorPermission } from '@/client/api/permission'
|
||||
import StorageAPI from '@/client/api/storage'
|
||||
import WorkspaceAPI from '@/client/api/workspace'
|
||||
import { swrConfig } from '@/client/options'
|
||||
import WorkspaceDelete from '@/components/workspace/workspace-delete'
|
||||
import WorkspaceEditName from '@/components/workspace/workspace-edit-name'
|
||||
import WorkspaceEditStorageCapacity from '@/components/workspace/workspace-edit-storage-capacity'
|
||||
import prettyBytes from '@/helpers/pretty-bytes'
|
||||
import { IconEdit, IconDelete, SectionSpinner } from '@/lib'
|
||||
|
||||
const EditButton = (props: IconButtonProps) => (
|
||||
<IconButton icon={<IconEdit />} {...props} />
|
||||
)
|
||||
|
||||
const Spacer = () => <div className={cx('grow')} />
|
||||
|
||||
const WorkspaceSettingsPage = () => {
|
||||
const { id } = useParams()
|
||||
const { data: workspace, error: workspaceError } = WorkspaceAPI.useGetById(
|
||||
id,
|
||||
swrConfig(),
|
||||
)
|
||||
const { data: storageUsage, error: storageUsageError } =
|
||||
StorageAPI.useGetWorkspaceUsage(id, swrConfig())
|
||||
const hasEditPermission = useMemo(
|
||||
() => workspace && geEditorPermission(workspace.permission),
|
||||
[workspace],
|
||||
)
|
||||
const [isNameModalOpen, setIsNameModalOpen] = useState(false)
|
||||
const [isStorageCapacityModalOpen, setIsStorageCapacityModalOpen] =
|
||||
useState(false)
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const sectionClassName = cx('flex', 'flex-col', 'gap-1', 'py-1.5')
|
||||
const rowClassName = cx(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-1',
|
||||
`h-[40px]`,
|
||||
)
|
||||
|
||||
if (workspaceError) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
return <SectionSpinner />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{workspace.name}</title>
|
||||
</Helmet>
|
||||
<div className={cx('flex', 'flex-col', 'gap-0')}>
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Storage</span>
|
||||
{storageUsageError && <span>Failed to load storage usage.</span>}
|
||||
{storageUsage && !storageUsageError && (
|
||||
<>
|
||||
<span>
|
||||
{prettyBytes(storageUsage.bytes)} of{' '}
|
||||
{prettyBytes(storageUsage.maxBytes)} used
|
||||
</span>
|
||||
<Progress value={storageUsage.percentage} hasStripe />
|
||||
</>
|
||||
)}
|
||||
{!storageUsage && !storageUsageError && (
|
||||
<>
|
||||
<span>Calculating…</span>
|
||||
<Progress value={0} hasStripe />
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<div className={rowClassName}>
|
||||
<span>Storage capacity</span>
|
||||
<Spacer />
|
||||
<span>{prettyBytes(workspace.storageCapacity)}</span>
|
||||
<EditButton
|
||||
aria-label=""
|
||||
isDisabled={!hasEditPermission}
|
||||
onClick={() => {
|
||||
setIsStorageCapacityModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider className={cx('mb-1.5')} />
|
||||
<span className={cx('font-bold')}>Basics</span>
|
||||
<div className={rowClassName}>
|
||||
<span>Name</span>
|
||||
<Spacer />
|
||||
<span>{workspace.name}</span>
|
||||
<EditButton
|
||||
aria-label=""
|
||||
isDisabled={!hasEditPermission}
|
||||
onClick={() => {
|
||||
setIsNameModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={sectionClassName}>
|
||||
<span className={cx('font-bold')}>Advanced</span>
|
||||
<div className={rowClassName}>
|
||||
<span>Delete permanently</span>
|
||||
<Spacer />
|
||||
<IconButton
|
||||
icon={<IconDelete />}
|
||||
variant="solid"
|
||||
colorScheme="red"
|
||||
isDisabled={!hasEditPermission}
|
||||
aria-label=""
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceEditName
|
||||
open={isNameModalOpen}
|
||||
workspace={workspace}
|
||||
onClose={() => setIsNameModalOpen(false)}
|
||||
/>
|
||||
<WorkspaceEditStorageCapacity
|
||||
open={isStorageCapacityModalOpen}
|
||||
workspace={workspace}
|
||||
onClose={() => setIsStorageCapacityModalOpen(false)}
|
||||
/>
|
||||
<WorkspaceDelete
|
||||
open={isDeleteModalOpen}
|
||||
workspace={workspace}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceSettingsPage
|
||||
Reference in New Issue
Block a user