diff --git a/web-app/client/src/graphql/operations/mutations/updateUser.ts b/web-app/client/src/graphql/operations/mutations/updateUser.ts new file mode 100644 index 00000000..8218c777 --- /dev/null +++ b/web-app/client/src/graphql/operations/mutations/updateUser.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_USER = gql` + mutation updateUser($props: UpdatingUserProps!) { + updateUser(props: $props) { + message + } + } +`; diff --git a/web-app/client/src/graphql/operations/queries/getOwnDatasets.ts b/web-app/client/src/graphql/operations/queries/getOwnDatasets.ts new file mode 100644 index 00000000..3366cea3 --- /dev/null +++ b/web-app/client/src/graphql/operations/queries/getOwnDatasets.ts @@ -0,0 +1,34 @@ +import { gql } from '@apollo/client'; + +export const GET_OWN_DATASETS = gql` + query getOwnDatasets($props: DatasetsQueryProps!) { + user { + datasets(props: $props) { + total + data { + fileID + fileName + hasHeader + delimiter + supportedPrimitives + rowsCount + fileSize + fileFormat { + inputFormat + tidColumnIndex + itemColumnIndex + hasTid + } + user { + fullName + } + countOfColumns + isBuiltIn + createdAt + originalFileName + numberOfUses + } + } + } + } +`; diff --git a/web-app/client/src/graphql/operations/queries/getOwnTasks.ts b/web-app/client/src/graphql/operations/queries/getOwnTasks.ts new file mode 100644 index 00000000..4d3a3017 --- /dev/null +++ b/web-app/client/src/graphql/operations/queries/getOwnTasks.ts @@ -0,0 +1,40 @@ +import { gql } from '@apollo/client'; + +export const GET_OWN_TASKS = gql` + query getOwnTasks($props: TasksQueryProps!) { + user { + tasks(props: $props) { + total + data { + taskID + state { + ... on TaskState { + user { + fullName + } + processStatus + phaseName + currentPhase + progress + maxPhase + isExecuted + elapsedTime + createdAt + } + } + + data { + baseConfig { + algorithmName + type + } + } + + dataset { + originalFileName + } + } + } + } + } +`; diff --git a/web-app/client/src/pages/me/[tab].tsx b/web-app/client/src/pages/me/[tab].tsx new file mode 100644 index 00000000..7a7f7835 --- /dev/null +++ b/web-app/client/src/pages/me/[tab].tsx @@ -0,0 +1,42 @@ +import { useRouter } from 'next/router'; +import UserIcon from '@assets/icons/user.svg?component'; +import TabsLayout from '@components/TabsLayout'; +import { useAuthContext } from '@hooks/useAuthContext'; +import styles from '@styles/Me.module.scss'; +import tabs from 'src/routes/UserCabinet/tabs'; +import { NextPageWithLayout } from 'types/pageWithLayout'; + +const AdminPanel: NextPageWithLayout = () => { + const router = useRouter(); + const { user } = useAuthContext(); + + const currentTab = + tabs.find((tab) => router.query.tab === tab.pathname) ?? tabs[0]; + + const Component = currentTab.component; + + return ( + router.push(`/me/${pathname}`)} + beforeTabs={ + + + + + + + + {user?.name} + {user?.email} + + + } + > + + + ); +}; + +export default AdminPanel; diff --git a/web-app/client/src/pages/me/index.ts b/web-app/client/src/pages/me/index.ts new file mode 100644 index 00000000..19fcd02b --- /dev/null +++ b/web-app/client/src/pages/me/index.ts @@ -0,0 +1,17 @@ +import { GetServerSideProps, NextPage } from 'next'; +import tabs from 'src/routes/UserCabinet/tabs'; + +const UserCabinet: NextPage = () => { + return null; +}; + +export const getServerSideProps: GetServerSideProps = async () => { + return { + redirect: { + destination: `/me/${tabs[0].pathname}`, + permanent: true, + }, + }; +}; + +export default UserCabinet; diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/AccountSettings.module.scss b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/AccountSettings.module.scss new file mode 100644 index 00000000..be50aaaf --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/AccountSettings.module.scss @@ -0,0 +1,18 @@ +@import "styles/common"; + +.accountSettingsTab { + max-width: 464px; + + .title { + margin: 0 0 32px; + } + + .additionalActions { + display: flex; + gap: 16px; + + button { + width: 100%; + } + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/AccountSettings.tsx b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/AccountSettings.tsx new file mode 100644 index 00000000..c54a67ef --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/AccountSettings.tsx @@ -0,0 +1,43 @@ +import { useQuery } from '@apollo/client'; +import { FC, useState } from 'react'; +import Button from '@components/Button'; +import { getUser } from '@graphql/operations/queries/__generated__/getUser'; +import { GET_USER } from '@graphql/operations/queries/getUser'; +import ChangePasswordModal from './components/ChangePasswordModal'; +import ProfileForm from './components/ProfileForm'; +import styles from './AccountSettings.module.scss'; + +const AccountSettings: FC = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const { data } = useQuery(GET_USER, { + variables: { + userID: undefined, + }, + }); + + const user = data?.user; + + if (!user) { + return null; + } + + return ( + + Account Settings + + + setIsModalOpen(true)}> + Change Password + + + Delete Account + + + {isModalOpen && ( + setIsModalOpen(false)} /> + )} + + ); +}; + +export default AccountSettings; diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/ChangePasswordModal.module.scss b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/ChangePasswordModal.module.scss new file mode 100644 index 00000000..fa5e3ab4 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/ChangePasswordModal.module.scss @@ -0,0 +1,21 @@ +@import "styles/common"; + +.changePasswordModal { + .title { + text-align: center; + margin: 0 0 32px; + } + + form { + display: flex; + flex-direction: column; + gap: 24px; + margin: 0 0 32px; + } + + .actions { + display: flex; + justify-content: center; + gap: 16px; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/ChangePasswordModal.tsx b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/ChangePasswordModal.tsx new file mode 100644 index 00000000..853a1ed7 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/ChangePasswordModal.tsx @@ -0,0 +1,93 @@ +import { useMutation } from '@apollo/client'; +import { FC, useId } from 'react'; +import { useForm } from 'react-hook-form'; +import isStrongPassword from 'validator/lib/isStrongPassword'; +import Button from '@components/Button'; +import { Text } from '@components/Inputs'; +import ModalContainer, { ModalProps } from '@components/ModalContainer'; +import { + changePassword, + changePasswordVariables, +} from '@graphql/operations/mutations/__generated__/changePassword'; +import { CHANGE_PASSWORD } from '@graphql/operations/mutations/changePassword'; +import hashPassword from '@utils/hashPassword'; +import styles from './ChangePasswordModal.module.scss'; + +type Inputs = { + oldPassword: string; + newPassword: string; + repeatPassword: string; +}; + +const ChangePasswordModal: FC = ({ onClose }) => { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm(); + const formId = useId(); + const [changePassword] = useMutation( + CHANGE_PASSWORD, + ); + + const onSubmit = handleSubmit(async (values) => { + try { + await changePassword({ + variables: { + currentPwdHash: hashPassword(values.oldPassword), + newPwdHash: hashPassword(values.newPassword), + }, + }); + location.reload(); + } catch (e) {} + }); + + return ( + + Change password + + + isStrongPassword(value) || 'Weak password', + })} + error={errors.newPassword?.message} + /> + isStrongPassword(value) || 'Weak password', + isSame: (value, { newPassword }) => + value === newPassword || 'Passwords do not match', + }, + })} + error={errors.repeatPassword?.message} + /> + + + Cancel + + Update password + + + + ); +}; + +export default ChangePasswordModal; diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/index.ts b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/index.ts new file mode 100644 index 00000000..73e9d820 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ChangePasswordModal/index.ts @@ -0,0 +1 @@ +export { default } from './ChangePasswordModal'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/ProfileForm.module.scss b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/ProfileForm.module.scss new file mode 100644 index 00000000..dee05167 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/ProfileForm.module.scss @@ -0,0 +1,8 @@ +@import "styles/common"; + +.profileForm { + display: flex; + flex-direction: column; + gap: 24px; + margin: 0 0 16px; +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/ProfileForm.tsx b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/ProfileForm.tsx new file mode 100644 index 00000000..6c574676 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/ProfileForm.tsx @@ -0,0 +1,138 @@ +import { useMutation } from '@apollo/client'; +import { countries } from 'countries-list'; +import _ from 'lodash'; +import { FC, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import Button from '@components/Button'; +import { Text } from '@components/Inputs'; +import { ControlledSelect } from '@components/Inputs/Select'; +import { + updateUser, + updateUserVariables, +} from '@graphql/operations/mutations/__generated__/updateUser'; +import { UPDATE_USER } from '@graphql/operations/mutations/updateUser'; +import { getUser_user } from '@graphql/operations/queries/__generated__/getUser'; +import styles from './ProfileForm.module.scss'; + +const countryNames = Object.entries(countries).map(([, country]) => country); + +type Inputs = { + fullName: string; + country: string; + companyOrAffiliation: string; + occupation: string; +}; + +interface Props { + user: getUser_user; +} + +const ProfileForm: FC = ({ user }) => { + const { + control, + reset, + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm(); + + const [updateUser] = useMutation( + UPDATE_USER, + ); + + useEffect(() => { + reset( + _.pick(user, [ + 'fullName', + 'country', + 'occupation', + 'companyOrAffiliation', + ]), + ); + }, [user, reset]); + + const onSubmit = handleSubmit(async (values) => { + try { + await updateUser({ + variables: { + props: values, + }, + }); + location.reload(); + } catch (e) {} + }); + + return ( + + + + ({ + label: `${emoji} ${native}`, + value: name, + }))} + rules={{ + required: 'Required', + }} + error={errors.country?.message} + /> + + + + + Update profile + + + ); +}; + +export default ProfileForm; diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/index.ts b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/index.ts new file mode 100644 index 00000000..663ef61a --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/components/ProfileForm/index.ts @@ -0,0 +1 @@ +export { default } from './ProfileForm'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/index.ts b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/index.ts new file mode 100644 index 00000000..0e910a5d --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/AccountSettings/index.ts @@ -0,0 +1 @@ +export { default } from './AccountSettings'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/Overview.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Overview/Overview.module.scss new file mode 100644 index 00000000..491943bc --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/Overview.module.scss @@ -0,0 +1,12 @@ +@import "styles/common"; + +.overviewTab { + display: flex; + justify-content: space-between; + gap: 64px; + + .left { + max-width: 980px; + width: 100%; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/Overview.tsx b/web-app/client/src/routes/UserCabinet/tabs/Overview/Overview.tsx new file mode 100644 index 00000000..e92cae10 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/Overview.tsx @@ -0,0 +1,45 @@ +import { useQuery } from '@apollo/client'; +import { FC } from 'react'; +import { getUser } from '@graphql/operations/queries/__generated__/getUser'; +import { GET_USER } from '@graphql/operations/queries/getUser'; +import Common from './components/Common'; +import Files from './components/Files'; +import Tasks from './components/Tasks'; +import styles from './Overview.module.scss'; + +const Overview: FC = () => { + const { data } = useQuery(GET_USER, { + variables: { + userID: undefined, + }, + fetchPolicy: 'network-only', + }); + + const user = data?.user; + + if (!user) { + return null; + } + + const { datasets, tasks, remainingDiskSpace, reservedDiskSpace } = user; + + return ( + + + + + + + + ); +}; + +export default Overview; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/Common.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/Common.module.scss new file mode 100644 index 00000000..a583d8b7 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/Common.module.scss @@ -0,0 +1,13 @@ +@import "styles/common"; + +.commonSection { + .title { + margin: 0 0 32px; + } + + .statistics { + display: flex; + gap: 48px; + margin: 0 0 64px; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/Common.tsx b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/Common.tsx new file mode 100644 index 00000000..00565d06 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/Common.tsx @@ -0,0 +1,28 @@ +import prettyBytes from 'pretty-bytes'; +import { FC } from 'react'; +import Statistic from '../Statistic'; +import styles from './Common.module.scss'; + +interface Props { + files: number; + tasks: number; + freeSpace: number; +} + +const Common: FC = ({ files, tasks, freeSpace }) => { + return ( + + Overview + + + + + + + ); +}; + +export default Common; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/index.ts new file mode 100644 index 00000000..6f8e9af8 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Common/index.ts @@ -0,0 +1 @@ +export { default } from './Common'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/FileItem.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/FileItem.module.scss new file mode 100644 index 00000000..b1b1efb7 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/FileItem.module.scss @@ -0,0 +1,48 @@ +@import "styles/common"; + +.fileItem { + display: flex; + align-items: center; + gap: 16px; + padding: 8px; + background: $white-25; + border: 1px solid $black-25; + border-radius: 8px; + + .iconContainer { + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: 48px; + height: 48px; + background: $black-10; + border-radius: 8px; + + svg path { + fill: $black-50; + } + } + + .middle { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; + + .fileName { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .fileSize { + color: $black-50; + } + } + + .percentage { + color: $primary-0; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/FileItem.tsx b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/FileItem.tsx new file mode 100644 index 00000000..90a497b9 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/FileItem.tsx @@ -0,0 +1,32 @@ +import prettyBytes from 'pretty-bytes'; +import { FC } from 'react'; +import FileIcon from '@assets/icons/file.svg?component'; +import { getUser_user_datasets_data } from '@graphql/operations/queries/__generated__/getUser'; +import styles from './FileItem.module.scss'; + +interface Props { + data: getUser_user_datasets_data; + percentage: number; +} + +const FileItem: FC = ({ + data: { originalFileName, fileSize }, + percentage = 69, +}) => { + return ( + + + + + + {originalFileName} + + {prettyBytes(fileSize, { binary: true })} + + + {(percentage * 100).toFixed(1)}% + + ); +}; + +export default FileItem; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/index.ts new file mode 100644 index 00000000..bc9838e7 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/FileItem/index.ts @@ -0,0 +1 @@ +export { default } from './FileItem'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/Files.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/Files.module.scss new file mode 100644 index 00000000..4db9b954 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/Files.module.scss @@ -0,0 +1,41 @@ +@import "styles/common"; + +.filesSection { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 356px; + + .top { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + margin: 0 0 48px; + + .chart { + width: 256px; + height: 128px; + } + + .stats { + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + bottom: 0; + + p { + color: $black-50; + } + } + } + + .files { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/Files.tsx b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/Files.tsx new file mode 100644 index 00000000..1c2eccee --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/Files.tsx @@ -0,0 +1,60 @@ +import prettyBytes from 'pretty-bytes'; +import { FC } from 'react'; +// @ts-expect-error no type declarations available +import arc from 'svg-arc'; +import colors from '@constants/colors'; +import { getUser_user_datasets_data } from '@graphql/operations/queries/__generated__/getUser'; +import FileItem from '../FileItem'; +import styles from './Files.module.scss'; + +interface Props { + files: getUser_user_datasets_data[] | null; + reservedSpace: number; + usedSpace: number; +} + +const arcProps = { + x: 128, + y: 128, + R: 128, + r: 104, + start: -90, + end: 90, +}; + +const Files: FC = ({ files, reservedSpace, usedSpace }) => { + const percentage = usedSpace / reservedSpace; + const degreesForUsedSpace = percentage * 180; + + return ( + + + + + + + + {prettyBytes(usedSpace, { binary: true })} + used of {prettyBytes(reservedSpace, { binary: true })} + + + + {files?.map((file) => ( + + ))} + + + ); +}; + +export default Files; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/index.ts new file mode 100644 index 00000000..d9de2629 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Files/index.ts @@ -0,0 +1 @@ +export { default } from './Files'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/Statistic.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/Statistic.module.scss new file mode 100644 index 00000000..67f17bb4 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/Statistic.module.scss @@ -0,0 +1,12 @@ +@import "styles/common"; + +.statistic { + .label { + text-transform: uppercase; + color: $black-50; + } + + .value { + font-size: 32px; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/Statistic.tsx b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/Statistic.tsx new file mode 100644 index 00000000..942cd0dd --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/Statistic.tsx @@ -0,0 +1,18 @@ +import { FC, ReactNode } from 'react'; +import styles from './Statistic.module.scss'; + +interface Props { + label: string; + value: ReactNode; +} + +const Statistic: FC = ({ label, value }) => { + return ( + + {label} + {value} + + ); +}; + +export default Statistic; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/index.ts new file mode 100644 index 00000000..2391980a --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Statistic/index.ts @@ -0,0 +1 @@ +export { default } from './Statistic'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/Tasks.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/Tasks.module.scss new file mode 100644 index 00000000..3bedf2ba --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/Tasks.module.scss @@ -0,0 +1,13 @@ +@import "styles/common"; + +.tasksSection { + .title { + margin: 0 0 32px; + } + + .taskList { + display: flex; + flex-direction: column; + gap: 16px; + } +} diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/Tasks.tsx b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/Tasks.tsx new file mode 100644 index 00000000..20e88eef --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/Tasks.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; +import TaskItem from '@components/TaskItem'; +import { getUser_user_tasks_data } from '@graphql/operations/queries/__generated__/getUser'; +import styles from './Tasks.module.scss'; + +interface Props { + tasks: getUser_user_tasks_data[] | null; +} + +const Tasks: FC = ({ tasks }) => { + return ( + + Tasks + + {tasks?.map((task) => )} + + + ); +}; + +export default Tasks; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/index.ts new file mode 100644 index 00000000..40e8ad4e --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/components/Tasks/index.ts @@ -0,0 +1 @@ +export { default } from './Tasks'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Overview/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Overview/index.ts new file mode 100644 index 00000000..28c6138f --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Overview/index.ts @@ -0,0 +1 @@ +export { default } from './Overview'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/Tasks.tsx b/web-app/client/src/routes/UserCabinet/tabs/Tasks/Tasks.tsx new file mode 100644 index 00000000..d2154f7e --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/Tasks.tsx @@ -0,0 +1,88 @@ +import { Moment } from 'moment'; +import moment from 'moment'; +import { FC } from 'react'; +import TabLayout from '@components/TabLayout'; +import TaskItem from '@components/TaskItem'; +import { + getOwnTasks, + getOwnTasksVariables, + getOwnTasks_user_tasks_data, +} from '@graphql/operations/queries/__generated__/getOwnTasks'; +import { GET_OWN_TASKS } from '@graphql/operations/queries/getOwnTasks'; +import { + OrderDirection, + TasksQueryFilters, + TasksQueryOrderingParameter, +} from 'types/globalTypes'; +import FiltersModal from './components/FiltersModal'; +import OrderingModal from './components/OrderingModal'; + +export type Filters = { + searchString?: string; + elapsedTime: [number | undefined, number | undefined]; + period: [Moment | undefined, Moment | undefined]; +}; + +const defaultFilters: Filters = { + searchString: undefined, + elapsedTime: [undefined, undefined], + period: [undefined, undefined], +}; + +export type Ordering = { + parameter: TasksQueryOrderingParameter; + direction: OrderDirection; +}; + +const defaultOrdering: Ordering = { + parameter: TasksQueryOrderingParameter.CREATION_TIME, + direction: OrderDirection.DESC, +}; + +const filtersToApi = (filters: Filters): TasksQueryFilters => ({ + ...filters, + searchString: filters.searchString || undefined, + elapsedTime: filters.elapsedTime.some(Boolean) + ? { + from: filters.elapsedTime[0] ?? undefined, + to: filters.elapsedTime[1] ?? undefined, + } + : undefined, + period: filters.period.some(Boolean) + ? { + from: filters.period[0]?.toISOString() ?? undefined, + to: filters.period[1]?.toISOString() ?? undefined, + } + : undefined, +}); + +const Tasks: FC = () => ( + + title="Tasks" + query={GET_OWN_TASKS} + filters={{ + defaultValues: defaultFilters, + valuesToApi: filtersToApi, + storageToValues: { + period: (value) => + value.map((v?: string) => (v ? moment(v) : undefined)), + elapsedTime: (value) => value.map((v?: number) => v ?? undefined), + }, + modal: FiltersModal, + }} + ordering={{ + defaultValues: defaultOrdering, + modal: OrderingModal, + }} + getData={(result) => result?.user?.tasks} + itemRenderer={(item) => } + /> +); + +export default Tasks; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/FiltersModal.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/FiltersModal.module.scss new file mode 100644 index 00000000..c3b15c6e --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/FiltersModal.module.scss @@ -0,0 +1,6 @@ +@import "styles/common"; + +.periodInput { + position: relative; + z-index: 2; +} \ No newline at end of file diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/FiltersModal.tsx b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/FiltersModal.tsx new file mode 100644 index 00000000..9ef77c86 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/FiltersModal.tsx @@ -0,0 +1,53 @@ +import React, { FC } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { DateTime, NumberRange } from '@components/Inputs'; +import ListPropertiesModal from '@components/ListPropertiesModal'; +import { Filters } from '../../Tasks'; +import styles from './FiltersModal.module.scss'; + +interface Props { + onClose: () => void; + onApply: () => void; +} + +const FilterModal: FC = ({ onClose, onApply }) => { + const { control } = useFormContext(); + + return ( + { + onApply(); + onClose(); + }} + > + ( + + )} + /> + ( + + )} + /> + + ); +}; + +export default FilterModal; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/index.ts new file mode 100644 index 00000000..64d4d6f0 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/FiltersModal/index.ts @@ -0,0 +1 @@ +export { default } from './FiltersModal'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/OrderingModal.module.scss b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/OrderingModal.module.scss new file mode 100644 index 00000000..d19c4a5f --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/OrderingModal.module.scss @@ -0,0 +1 @@ +@import "styles/common"; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/OrderingModal.tsx b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/OrderingModal.tsx new file mode 100644 index 00000000..493283a6 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/OrderingModal.tsx @@ -0,0 +1,72 @@ +import React, { FC } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { Select } from '@components/Inputs'; +import ListPropertiesModal from '@components/ListPropertiesModal'; +import { OrderDirection, TasksQueryOrderingParameter } from 'types/globalTypes'; +import { Ordering } from '../../Tasks'; + +const parameterLabels: Partial> = { + CREATION_TIME: 'Created', + ELAPSED_TIME: 'Elapsed time', + STATUS: 'Status', +}; + +const directionLabels: Record = { + ASC: 'Ascending', + DESC: 'Descending', +}; + +const parameterOptions = Object.entries(parameterLabels).map( + ([value, label]) => ({ label, value }), +); + +const directionOptions = Object.entries(directionLabels).map( + ([value, label]) => ({ label, value }), +); + +interface Props { + onClose: () => void; + onApply: () => void; +} + +const OrderingModal: FC = ({ onClose, onApply }) => { + const { control } = useFormContext(); + + return ( + { + onApply(); + onClose(); + }} + > + ( + option.value === value)} + onChange={(option) => onChange(option?.value)} + options={parameterOptions} + /> + )} + /> + ( + option.value === value)} + onChange={(option) => onChange(option?.value)} + options={directionOptions} + /> + )} + /> + + ); +}; + +export default OrderingModal; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/index.ts new file mode 100644 index 00000000..cbcd085b --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/components/OrderingModal/index.ts @@ -0,0 +1 @@ +export { default } from './OrderingModal'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/Tasks/index.ts b/web-app/client/src/routes/UserCabinet/tabs/Tasks/index.ts new file mode 100644 index 00000000..40e8ad4e --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/Tasks/index.ts @@ -0,0 +1 @@ +export { default } from './Tasks'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/UploadedFiles.tsx b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/UploadedFiles.tsx new file mode 100644 index 00000000..3d07a41f --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/UploadedFiles.tsx @@ -0,0 +1,90 @@ +import { Moment } from 'moment'; +import moment from 'moment'; +import { FC } from 'react'; +import DatasetItem from '@components/DatasetItem'; +import TabLayout from '@components/TabLayout'; +import { + getOwnDatasets, + getOwnDatasetsVariables, + getOwnDatasets_user_datasets_data, +} from '@graphql/operations/queries/__generated__/getOwnDatasets'; +import { GET_OWN_DATASETS } from '@graphql/operations/queries/getOwnDatasets'; +import { + DatasetsQueryFilters, + DatasetsQueryOrderingParameter, + OrderDirection, +} from 'types/globalTypes'; +import FiltersModal from './components/FiltersModal'; +import OrderingModal from './components/OrderingModal'; + +export type Filters = { + searchString?: string; + fileSize: [number | undefined, number | undefined]; + period: [Moment | undefined, Moment | undefined]; +}; + +const defaultFilters: Filters = { + searchString: undefined, + fileSize: [undefined, undefined], + period: [undefined, undefined], +}; + +export type Ordering = { + parameter: DatasetsQueryOrderingParameter; + direction: OrderDirection; +}; + +const defaultOrdering: Ordering = { + parameter: DatasetsQueryOrderingParameter.CREATION_TIME, + direction: OrderDirection.DESC, +}; + +const formatToRange = ( + values: [T | undefined, T | undefined], + formatter: (value: T) => TResult, +) => + values.some(Boolean) + ? { + from: values[0] ? formatter(values[0]) : undefined, + to: values[1] ? formatter(values[1]) : undefined, + } + : undefined; + +const filtersToApi = (filters: Filters): DatasetsQueryFilters => ({ + ...filters, + includeBuiltIn: false, + searchString: filters.searchString || undefined, + fileSize: formatToRange(filters.fileSize, (value) => value * 2 ** 20), + period: formatToRange(filters.period, (value) => value.toISOString()), +}); + +const UploadedFiles: FC = () => ( + + title="Uploaded Files" + query={GET_OWN_DATASETS} + filters={{ + defaultValues: defaultFilters, + valuesToApi: filtersToApi, + storageToValues: { + period: (value) => + value.map((v?: string) => (v ? moment(v) : undefined)), + fileSize: (value) => value.map((v?: number) => v ?? undefined), + }, + modal: FiltersModal, + }} + ordering={{ + defaultValues: defaultOrdering, + modal: OrderingModal, + }} + getData={(result) => result?.user?.datasets} + itemRenderer={(item) => } + /> +); + +export default UploadedFiles; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/FilePropertiesModal.module.scss b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/FilePropertiesModal.module.scss new file mode 100644 index 00000000..5716482d --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/FilePropertiesModal.module.scss @@ -0,0 +1,6 @@ +@import "styles/common"; + +.header { + text-align: center; + margin: 0 0 32px; +} \ No newline at end of file diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/FilePropertiesModal.tsx b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/FilePropertiesModal.tsx new file mode 100644 index 00000000..1c3d755f --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/FilePropertiesModal.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react'; +import { FilePropsList } from '@components/FilePropertiesModal/tabs/PropertiesTab'; +import ModalContainer, { ModalProps } from '@components/ModalContainer'; +import { AllowedDataset } from 'types/algorithms'; +import styles from './FilePropertiesModal.module.scss'; + +type Props = ModalProps & { data: AllowedDataset }; + +const FilePropertiesModal: FC = ({ onClose, ...props }) => { + return ( + + File Properties + + + ); +}; + +export default FilePropertiesModal; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/index.ts b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/index.ts new file mode 100644 index 00000000..8acb882b --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FilePropertiesModal/index.ts @@ -0,0 +1 @@ +export { default } from './FilePropertiesModal'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/FiltersModal.module.scss b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/FiltersModal.module.scss new file mode 100644 index 00000000..c3b15c6e --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/FiltersModal.module.scss @@ -0,0 +1,6 @@ +@import "styles/common"; + +.periodInput { + position: relative; + z-index: 2; +} \ No newline at end of file diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/FiltersModal.tsx b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/FiltersModal.tsx new file mode 100644 index 00000000..174aabac --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/FiltersModal.tsx @@ -0,0 +1,53 @@ +import React, { FC } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { DateTime, NumberRange } from '@components/Inputs'; +import ListPropertiesModal from '@components/ListPropertiesModal'; +import { Filters } from '../../UploadedFiles'; +import styles from './FiltersModal.module.scss'; + +interface Props { + onClose: () => void; + onApply: () => void; +} + +const FilterModal: FC = ({ onClose, onApply }) => { + const { control } = useFormContext(); + + return ( + { + onApply(); + onClose(); + }} + > + ( + + )} + /> + ( + + )} + /> + + ); +}; + +export default FilterModal; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/index.ts b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/index.ts new file mode 100644 index 00000000..64d4d6f0 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/FiltersModal/index.ts @@ -0,0 +1 @@ +export { default } from './FiltersModal'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/OrderingModal.module.scss b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/OrderingModal.module.scss new file mode 100644 index 00000000..d19c4a5f --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/OrderingModal.module.scss @@ -0,0 +1 @@ +@import "styles/common"; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/OrderingModal.tsx b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/OrderingModal.tsx new file mode 100644 index 00000000..9b7f44aa --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/OrderingModal.tsx @@ -0,0 +1,76 @@ +import React, { FC } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { Select } from '@components/Inputs'; +import ListPropertiesModal from '@components/ListPropertiesModal'; +import { + DatasetsQueryOrderingParameter, + OrderDirection, +} from 'types/globalTypes'; +import { Ordering } from '../../UploadedFiles'; + +const parameterLabels: Partial> = + { + CREATION_TIME: 'Uploaded', + FILE_NAME: 'Name', + FILE_SIZE: 'Size', + }; + +const directionLabels: Record = { + ASC: 'Ascending', + DESC: 'Descending', +}; + +const parameterOptions = Object.entries(parameterLabels).map( + ([value, label]) => ({ label, value }), +); + +const directionOptions = Object.entries(directionLabels).map( + ([value, label]) => ({ label, value }), +); + +interface Props { + onClose: () => void; + onApply: () => void; +} + +const OrderingModal: FC = ({ onClose, onApply }) => { + const { control } = useFormContext(); + + return ( + { + onApply(); + onClose(); + }} + > + ( + option.value === value)} + onChange={(option) => onChange(option?.value)} + options={parameterOptions} + /> + )} + /> + ( + option.value === value)} + onChange={(option) => onChange(option?.value)} + options={directionOptions} + /> + )} + /> + + ); +}; + +export default OrderingModal; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/index.ts b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/index.ts new file mode 100644 index 00000000..cbcd085b --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/components/OrderingModal/index.ts @@ -0,0 +1 @@ +export { default } from './OrderingModal'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/index.ts b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/index.ts new file mode 100644 index 00000000..3717eaa7 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/UploadedFiles/index.ts @@ -0,0 +1 @@ +export { default } from './UploadedFiles'; diff --git a/web-app/client/src/routes/UserCabinet/tabs/index.tsx b/web-app/client/src/routes/UserCabinet/tabs/index.tsx new file mode 100644 index 00000000..9863cff1 --- /dev/null +++ b/web-app/client/src/routes/UserCabinet/tabs/index.tsx @@ -0,0 +1,58 @@ +import dynamic from 'next/dynamic'; +import { ComponentType, ReactNode } from 'react'; +import ChartIcon from '@assets/icons/chart.svg?component'; +import FileIcon from '@assets/icons/file.svg?component'; +import GearIcon from '@assets/icons/gear.svg?component'; +import UserIcon from '@assets/icons/user.svg?component'; + +export type TabConfig = { + pathname: string; + label: string; + icon: ReactNode; + component: ComponentType; +}; + +const OverviewComponent = dynamic(() => import('./Overview'), { + ssr: false, +}); + +const AccountSettings = dynamic(() => import('./AccountSettings'), { + ssr: false, +}); + +const UploadedFiles = dynamic(() => import('./UploadedFiles'), { + ssr: false, +}); + +const Tasks = dynamic(() => import('./Tasks'), { + ssr: false, +}); + +const tabs: TabConfig[] = [ + { + pathname: 'overview', + label: 'Overview', + icon: , + component: OverviewComponent, + }, + { + pathname: 'account', + label: 'Account Settings', + icon: , + component: AccountSettings, + }, + { + pathname: 'tasks', + label: 'Tasks', + icon: , + component: Tasks, + }, + { + pathname: 'files', + label: 'Uploaded Files', + icon: , + component: UploadedFiles, + }, +]; + +export default tabs; diff --git a/web-app/client/src/styles/Me.module.scss b/web-app/client/src/styles/Me.module.scss new file mode 100644 index 00000000..c65c48d4 --- /dev/null +++ b/web-app/client/src/styles/Me.module.scss @@ -0,0 +1,33 @@ +@import "styles/common"; + +.beforeMenu { + display: flex; + margin: 0 0 32px; + + .iconContainer { + padding: 6px; + margin: 0 8px 0 0; + + .iconBackground { + display: flex; + justify-content: center; + align-items: center; + width: 60px; + height: 60px; + background: $black-50; + border-radius: 100vmax; + } + + .icon { + width: 32px; + height: 32px; + color: $white; + } + } + + .nameContainer { + small { + color: $black-50; + } + } +}
{(percentage * 100).toFixed(1)}%
used of {prettyBytes(reservedSpace, { binary: true })}
{label}
{value}