diff --git a/src/components/modals/datasets/create/index.tsx b/src/components/modals/datasets/create/index.tsx new file mode 100644 index 0000000..abcc0b0 --- /dev/null +++ b/src/components/modals/datasets/create/index.tsx @@ -0,0 +1,241 @@ +import {FunctionComponent, useState} from 'react' +import {ButtonRound} from '@/components/lib' +import {createDataset} from '@/pages/api/datasets' +import {Modal} from '../../modal' + +interface ICreateDatasetModalProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void +} + +const CreateDatasetModal: FunctionComponent = ({isOpen, onClose, onConfirm}) => { + const [formObject, setFormObject] = useState({ + name: '', + description: '', + manifest: '', + tags: '', + created_at: new Date() + }) + + const [tensors, setTensors] = useState([]) + + const handleChange = event => { + const {name, value} = event.target + + const newFormObject = formObject + newFormObject[name] = value + setFormObject(newFormObject) + } + + const mapTensorsToDict = () => { + const newTensors = {} + tensors.map(tensor => (newTensors[tensor.name] = {manifest: tensor.manifest, content: tensor.content})) + return newTensors + } + + const handleSubmit = () => { + const newDataset = { + name: formObject.name, + description: formObject.description, + manifest: formObject.manifest, + tags: formObject.tags.trim().split(','), + created_at: formObject.created_at, + tensors: {...mapTensorsToDict()} + } + + createDataset(newDataset).then(() => onConfirm()) + } + + const appendTensors = () => { + setTensors(prevState => [...prevState, {name: '', manifest: '', content: ''}]) + } + + const removeTensors = () => { + const newTensors = [...tensors] + newTensors.pop() + setTensors(newTensors) + } + + const toBase64 = file => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = () => resolve(reader.result) + reader.onerror = error => reject(error) + }) + + const handleTensors = async (index, event) => { + const {name, value, files} = event.target + const newTensors = [...tensors] + + if (name === 'content') { + const base64Content = await toBase64(files[0]) + newTensors[index] = {...newTensors[index], [name]: base64Content} + setTensors(newTensors) + } else { + newTensors[index] = {...newTensors[index], [name]: value} + setTensors(newTensors) + } + } + + const renderTensorInputs = () => { + return tensors.map((el, i) => ( +
+
+ + handleTensors(i, e)} + /> +
+
+ + handleTensors(i, e)} + /> +
+
+ + handleTensors(i, e)} + /> +
+
+ )) + } + + const Footer = () => ( + <> + + Cancel + + + Create + + + ) + + return ( + }> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Tensors + {renderTensorInputs()} +
+ + + + + + - + +
+
+
+ ) +} + +export {CreateDatasetModal} diff --git a/src/components/modals/datasets/delete/index.tsx b/src/components/modals/datasets/delete/index.tsx new file mode 100644 index 0000000..cbeec61 --- /dev/null +++ b/src/components/modals/datasets/delete/index.tsx @@ -0,0 +1,48 @@ +import {FunctionComponent} from 'react' +import Router from 'next/router' + +import {deleteDataset} from '@/pages/api/datasets' +import {Modal} from '../../modal' + +interface IDeleteDatasetModalProps { + id: string + isOpen: boolean + onClose: () => void + onConfirm: () => void +} + +const DeleteDatasetModal: FunctionComponent = ({id, isOpen, onClose, onConfirm}) => { + const handleSubmit = () => { + deleteDataset({id}).then(() => { + onConfirm() + Router.push('/datasets') + }) + } + + const Footer = () => ( + <> + + + + ) + + return ( + <> + }> +

Are you sure you want to delete this dataset?

+
+ + ) +} + +export {DeleteDatasetModal} diff --git a/src/components/modals/datasets/edit/index.tsx b/src/components/modals/datasets/edit/index.tsx new file mode 100644 index 0000000..92279af --- /dev/null +++ b/src/components/modals/datasets/edit/index.tsx @@ -0,0 +1,262 @@ +import {FunctionComponent, useEffect, useState} from 'react' +import {ButtonRound} from '@/components/lib' +import {editDataset} from '@/pages/api/datasets' +import {Modal} from '../../modal' +import {IDataset} from '@/types/datasets' + +interface IEditDatasetModalProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void + dataset: IDataset +} + +const EditDatasetModal: FunctionComponent = ({isOpen, onClose, onConfirm, dataset}) => { + const [formObject, setFormObject] = useState({ + name: dataset.name, + description: dataset.description, + manifest: dataset.manifest, + tags: dataset.tags.toString(), + created_at: dataset.createdAt + }) + + const [tensors, setTensors] = useState([]) + + useEffect(() => { + setTensors(mapDictToTensors()) + }, []) + + const handleChange = event => { + const {name, value} = event.target + + const newFormObject = formObject + newFormObject[name] = value + setFormObject(newFormObject) + } + + const mapTensorsToDict = () => { + const newTensors = {} + tensors.map(tensor => (newTensors[tensor.name] = {manifest: tensor.manifest, content: tensor.content})) + return newTensors + } + + const mapDictToTensors = () => { + const newTensors = [] + Object.entries(dataset.tensors).map(([name, tensor]) => + newTensors.push({name, manifest: tensor.manifest, content: ''}) + ) + return newTensors + } + + const handleSubmit = () => { + const newDataset = { + id: dataset?.id, + name: formObject.name, + description: formObject.description, + manifest: formObject.manifest, + tags: formObject.tags.trim().split(','), + created_at: formObject.created_at, + tensors: {...mapTensorsToDict()} + } + + editDataset(newDataset).then(() => onConfirm()) + } + + const appendTensors = () => { + setTensors(prevState => [...prevState, {name: '', manifest: '', content: ''}]) + } + + const removeTensors = () => { + const newTensors = [...tensors] + newTensors.pop() + setTensors(newTensors) + } + + const toBase64 = file => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = () => resolve(reader.result) + reader.onerror = error => reject(error) + }) + + const handleTensors = async (index, event) => { + const {name, value, files} = event.target + const newTensors = [...tensors] + + if (name === 'content') { + const base64Content = await toBase64(files[0]) + newTensors[index] = {...newTensors[index], [name]: base64Content} + setTensors(newTensors) + } else { + newTensors[index] = {...newTensors[index], [name]: value} + setTensors(newTensors) + } + } + + const renderTensorInputs = () => { + return tensors.map((el, i) => ( +
+
+ + handleTensors(i, e)} + /> +
+
+ + handleTensors(i, e)} + /> +
+
+ + handleTensors(i, e)} + /> +
+
+ )) + } + + const Footer = () => ( + <> + + Cancel + + + Edit + + + ) + + return ( + }> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Tensors + {renderTensorInputs()} +
+ + + + + + - + +
+
+
+ ) +} + +export {EditDatasetModal} diff --git a/src/pages/api/datasets.ts b/src/pages/api/datasets.ts new file mode 100644 index 0000000..fd15dec --- /dev/null +++ b/src/pages/api/datasets.ts @@ -0,0 +1,53 @@ +import axios from '@/utils/api-axios' +import { + ICreateDatasetResponse, + IFetchDatasetResponse, + IEditDatasetResponse, + IDeleteDatasetResponse +} from '@/types/api-responses' + +export const fetchDataset = async ({id}) => { + if (id !== undefined) { + const {data} = await axios.get(`/dcfl/datasets/${id}`) + return data + } +} + +export const createDataset = async ({name, description, manifest, tags, created_at, tensors}) => { + const payload = { + name, + description, + manifest, + tags, + created_at, + tensors + } + + const {data} = await axios.post('/dcfl/datasets', payload) + + return data +} + +export const editDataset = async ({id, name, description, manifest, tags, created_at, tensors}) => { + if (id !== undefined) { + const payload = { + id, + name, + description, + manifest, + tags, + created_at, + tensors + } + + const {data} = await axios.put(`/dcfl/datasets/${id}`, payload) + return data + } +} + +export const deleteDataset = async ({id}) => { + if (id !== undefined) { + const {data} = await axios.delete(`/dcfl/datasets/${id}`) + return data + } +} diff --git a/src/pages/datasets/[slug].tsx b/src/pages/datasets/[slug].tsx index 955281a..69a1d55 100644 --- a/src/pages/datasets/[slug].tsx +++ b/src/pages/datasets/[slug].tsx @@ -1,29 +1,73 @@ -import type {FunctionComponent} from 'react' +import {FunctionComponent, useState} from 'react' +import {useRouter} from 'next/router' +import {useQuery} from 'react-query' + import {Tag, ButtonGhost} from '@/components/lib' import {Tabs, TabList, Tab, TabPanels, TabPanel} from '@reach/tabs' import {FileIcon} from '@/components/icons/file' -const Dataset: FunctionComponent<{slug?: string}> = () => { - const DatasetData = { - title: 'Diabetes Study 01.289.301', - description: - 'This was a double-blind diabetes study done in coordination with UC Santa Barbara between July 1st, 2017 and January 1st, 2019.', - tags: ['diabetes', 'california', 'healthcare', 'UCSF', 'beekeeper'], - tensors: 2 - } +import {IDataset, ITensor} from '@/types/datasets' +import {fetchDataset} from '../api/datasets' +import {EditDatasetModal} from '@/components/modals/datasets/edit' +import {DeleteDatasetModal} from '@/components/modals/datasets/delete' + +const Dataset: FunctionComponent = () => { + const router = useRouter() + const {slug} = router.query + + const [openDeleteDatasetModal, setOpenDeleteDatasetModal] = useState(false) + const [openEditDatasetModal, setOpenEditDatasetModal] = useState(false) + + const {isLoading, data: dataset} = useQuery(['dataset', slug], () => fetchDataset({id: slug})) + + if (isLoading || dataset === undefined) return null - const {title, description, tags} = DatasetData + const {id, name, manifest, description, tags, tensors} = dataset + + const handleTensors = (name: string, tensor: ITensor) => ( + + + data + +
+ + +
+ + File: {name} + + + Shape: {JSON.stringify(tensor.shape)} + + + Type: {tensor.dtype} + + + Schema:{' '} + +
    + {tensor.manifest.split(',').map(el => ( +
  • {el}
  • + ))} +
+
+
+
+
+
+
+ ) return (
-

{title}

+

{name}

- - alert('Delete dataset')}> + setOpenDeleteDatasetModal(true)}> Delete dataset
@@ -37,59 +81,26 @@ const Dataset: FunctionComponent<{slug?: string}> = () => { ))}
+
+

Manifest

+

{manifest}

+

Tensors

- - - data - target - -
- - -
- - File: {' '} - diabetes_data.csv - - - Shape: [10000, 8] - - - Type: 64-bit floating point - - - Schema:{' '} - -
    -
  • id
  • -
  • birthdate
  • -
  • sex
  • -
  • diagnosis_date
  • -
  • type
  • -
  • weight
  • -
  • glucose_level
  • -
  • medications
  • -
-
-
- - Entities: 10,000 unique entities - - - Default Permissions: search-only - - - Permissions: 7 users, 2 groups - -
-
- -
Target
-
-
-
+ {Object.entries(tensors).map(([name, tensor]) => handleTensors(name, tensor))}
+ setOpenEditDatasetModal(false)} + isOpen={openEditDatasetModal} + onConfirm={() => setOpenEditDatasetModal(false)} + dataset={dataset} + /> + setOpenDeleteDatasetModal(false)} + isOpen={openDeleteDatasetModal} + onConfirm={() => setOpenDeleteDatasetModal(false)} + />
) } diff --git a/src/pages/datasets/index.tsx b/src/pages/datasets/index.tsx index 6722530..18c5ada 100644 --- a/src/pages/datasets/index.tsx +++ b/src/pages/datasets/index.tsx @@ -1,8 +1,11 @@ -import type {FunctionComponent} from 'react' +import {FunctionComponent, useState} from 'react' import {DatasetCard} from '@/components/pages/datasets/cards/datasets' import {ArrowForward} from '@/components/icons/arrows' +import {CreateDatasetModal} from '@/components/modals/datasets/create' const Datasets: FunctionComponent = () => { + const [openCreateDatasetModal, setOpenCreateDatasetModal] = useState(false) + const sections = [ {title: 'Permissions changes', value: 19, text: 'requests'}, {title: 'Budget changes', value: 5, text: 'requests'}, @@ -44,7 +47,7 @@ const Datasets: FunctionComponent = () => {

Datasets

-
@@ -69,6 +72,11 @@ const Datasets: FunctionComponent = () => { ))} + setOpenCreateDatasetModal(false)} + onConfirm={() => setOpenCreateDatasetModal(false)} + />
) } diff --git a/src/types/api-responses.ts b/src/types/api-responses.ts index 1650701..5f4f64a 100644 --- a/src/types/api-responses.ts +++ b/src/types/api-responses.ts @@ -1,4 +1,5 @@ import {IGroup, IRole, IUser} from './users' +import {IDataset} from './datasets' export type IFetchUsersResponse = IUser[] @@ -10,6 +11,8 @@ export type IFetchGroupsResponse = IGroup[] export type IFetchGroupResponse = IRole +export type IFetchDatasetResponse = IDataset + export interface IFetchLoginResponse { token: string key: string @@ -24,6 +27,18 @@ export interface ICreateGroupResponse { message: string } +export interface ICreateDatasetResponse { + dataset: IDataset +} + +export interface IEditDatasetResponse { + message: string +} + +export interface IDeleteDatasetResponse { + message: string +} + export interface ICreateRoleResponse { message: string } diff --git a/src/types/datasets.ts b/src/types/datasets.ts new file mode 100644 index 0000000..004b2a8 --- /dev/null +++ b/src/types/datasets.ts @@ -0,0 +1,16 @@ +export interface IDataset { + id?: string + name: string + description: string + manifest: string + tags: string[] + createdAt: Date + tensors?: Record +} + +export interface ITensor { + manifest: string + id?: string + shape: number[] + dtype: string +}