diff --git a/visualization_tool/public/icons/filter.png b/visualization_tool/public/icons/filter.png new file mode 100644 index 00000000..1aa674c4 Binary files /dev/null and b/visualization_tool/public/icons/filter.png differ diff --git a/visualization_tool/public/icons/sort.png b/visualization_tool/public/icons/sort.png new file mode 100644 index 00000000..501504f2 Binary files /dev/null and b/visualization_tool/public/icons/sort.png differ diff --git a/visualization_tool/src/App.tsx b/visualization_tool/src/App.tsx index 35bfa8f3..cc1b837c 100644 --- a/visualization_tool/src/App.tsx +++ b/visualization_tool/src/App.tsx @@ -1,5 +1,5 @@ import {useState} from 'react'; -import {Resource} from './types/resources'; +import {Resource, availableResourceTypes} from './types/resources'; import Navbar from './components/Navbar/Navbar'; import ControlMenu from './components/ControlMenu/ControlMenu'; @@ -8,12 +8,25 @@ import ResourcesList from './components/ResourcesList/ResourcesList'; function App() { const [resources, setResources] = useState([]); const [searchQuery, setSearchQuery] = useState(''); + const [sortAttribute, setSortAttribute] = useState('date'); + const [allowedTypes, setAllowedTypes] = useState( + availableResourceTypes + ); return ( <>
- - + +
); diff --git a/visualization_tool/src/components/ControlMenu/ControlMenu.css b/visualization_tool/src/components/ControlMenu/ControlMenu.css index a97ac387..d89c7f31 100644 --- a/visualization_tool/src/components/ControlMenu/ControlMenu.css +++ b/visualization_tool/src/components/ControlMenu/ControlMenu.css @@ -13,17 +13,29 @@ .menu-item { width: 100%; - min-height: 180px; + min-height: 150px; background-color: white; border-radius: 5px; padding: 20px 10px 30px 10px; box-shadow: 0px 4px 6px 0px rgba(0, 0, 0, 0.15); + margin-bottom: 20px; } .menu-item__header { display: flex; align-items: center; - margin-bottom: 10px; + margin-bottom: 8px; +} + +.menu-item__content { + display: flex; + margin-top: 20px; + flex-direction: column; + gap: 10px; +} + +.menu-item__content label { + padding-left: 10px; } .menu-item h3 { diff --git a/visualization_tool/src/components/ControlMenu/ControlMenu.tsx b/visualization_tool/src/components/ControlMenu/ControlMenu.tsx index 02ab38f5..19b93b07 100644 --- a/visualization_tool/src/components/ControlMenu/ControlMenu.tsx +++ b/visualization_tool/src/components/ControlMenu/ControlMenu.tsx @@ -1,120 +1,27 @@ -import {useState, useRef} from 'react'; -import AddIcon from '@mui/icons-material/Add'; -import CloseIcon from '@mui/icons-material/Close'; -// validate schema with joi +import UploadMenu from './partials/UploadMenu'; +import SortMenu from './partials/SortMenu'; +import FilterMenu from './partials/FilterMenu'; import {Resource} from '../../types/resources'; -import {parseData} from './Controller'; - import './ControlMenu.css'; type ControlMenuProps = { setResources: React.Dispatch>; + setSortAttribute: React.Dispatch>; + setAllowedTypes: React.Dispatch>; }; -const ControlMenu = ({setResources}: ControlMenuProps) => { - const fileInput = useRef(null); - const [files, setFiles] = useState([]); - const [error, setError] = useState(null); - +const ControlMenu = ({ + setResources, + setSortAttribute, + setAllowedTypes, +}: ControlMenuProps) => { return (
-
-
- - - -

Upload

-
-
{ - e.preventDefault(); - setError(null); - const file = fileInput.current?.files?.[0]; - - if (!file) { - setError('No file selected'); - return; - } - // check if file is already uploaded - if (files.includes(file.name)) { - setError('File already uploaded'); - return; - } - - const reader = new FileReader(); - reader.readAsText(file); - reader.onload = e => { - const result = e.target?.result as string; - - try { - const data = JSON.parse(result); - const resources = parseData(data, file.name); - setResources((prevResources: Resource[]) => [ - ...prevResources, - ...resources, - ]); - setFiles([...files, file.name]); - } catch (err) { - setError('Invalid file'); - return; - } - }; - }} - > - - -
- {error &&

{error}

} - {files.length > 0 && ( -
- {files.map(file => ( -
-

{file}

- -
- ))} -
- )} -
+ + +
); }; diff --git a/visualization_tool/src/components/ControlMenu/partials/FilterMenu.tsx b/visualization_tool/src/components/ControlMenu/partials/FilterMenu.tsx new file mode 100644 index 00000000..9c9e09bb --- /dev/null +++ b/visualization_tool/src/components/ControlMenu/partials/FilterMenu.tsx @@ -0,0 +1,46 @@ +import {availableResourceTypes} from '../../../types/resources'; + +type FilterMenuProps = { + setAllowedTypes: React.Dispatch>; +}; + +const FilterMenu = ({setAllowedTypes}: FilterMenuProps) => { + return ( +
+
+ + + +

Resource Type

+
+ {availableResourceTypes.length > 0 && ( +
+ {availableResourceTypes.map(type => { + return ( +
+ { + if (e.target.checked) { + setAllowedTypes(prevTypes => [...prevTypes, type]); + } else { + setAllowedTypes(prevTypes => { + return prevTypes.filter(prevType => prevType !== type); + }); + } + }} + /> + +
+ ); + })} +
+ )} +
+ ); +}; + +export default FilterMenu; diff --git a/visualization_tool/src/components/ControlMenu/partials/SortMenu.tsx b/visualization_tool/src/components/ControlMenu/partials/SortMenu.tsx new file mode 100644 index 00000000..af37e1f7 --- /dev/null +++ b/visualization_tool/src/components/ControlMenu/partials/SortMenu.tsx @@ -0,0 +1,46 @@ +type SortMenuProps = { + setSortAttribute: React.Dispatch>; +}; + +const SortMenu = ({setSortAttribute}: SortMenuProps) => { + return ( +
+
+ + + +

Sort

+
+
+
+ { + setSortAttribute(e.target.value); + }} + /> + +
+ +
+ { + setSortAttribute(e.target.value); + }} + /> + +
+
+
+ ); +}; + +export default SortMenu; diff --git a/visualization_tool/src/components/ControlMenu/partials/UploadMenu.tsx b/visualization_tool/src/components/ControlMenu/partials/UploadMenu.tsx new file mode 100644 index 00000000..74d0d94a --- /dev/null +++ b/visualization_tool/src/components/ControlMenu/partials/UploadMenu.tsx @@ -0,0 +1,115 @@ +import {useState, useRef} from 'react'; +import AddIcon from '@mui/icons-material/Add'; +import CloseIcon from '@mui/icons-material/Close'; + +import {Resource} from '../../../types/resources'; +import {parseData} from '../Controller'; + +type UploadMenuProps = { + setResources: React.Dispatch>; +}; + +const UploadMenu = ({setResources}: UploadMenuProps) => { + const fileInput = useRef(null); + const [files, setFiles] = useState([]); + const [error, setError] = useState(null); + return ( +
+
+ + + +

Upload

+
+
{ + e.preventDefault(); + setError(null); + const file = fileInput.current?.files?.[0]; + + if (!file) { + setError('No file selected'); + return; + } + // check if file is already uploaded + if (files.includes(file.name)) { + setError('File already uploaded'); + return; + } + + const reader = new FileReader(); + reader.readAsText(file); + reader.onload = e => { + const result = e.target?.result as string; + + try { + const data = JSON.parse(result); + const resources = parseData(data, file.name); + setResources((prevResources: Resource[]) => [ + ...prevResources, + ...resources, + ]); + setFiles([...files, file.name]); + } catch (err) { + setError('Invalid file'); + return; + } + }; + }} + > + + +
+ {error &&

{error}

} + {files.length > 0 && ( +
+ {files.map(file => ( +
+

{file}

+ +
+ ))} +
+ )} +
+ ); +}; + +export default UploadMenu; diff --git a/visualization_tool/src/components/ResourcesList/ResourcesList.tsx b/visualization_tool/src/components/ResourcesList/ResourcesList.tsx index 3c89f368..918da14a 100644 --- a/visualization_tool/src/components/ResourcesList/ResourcesList.tsx +++ b/visualization_tool/src/components/ResourcesList/ResourcesList.tsx @@ -4,10 +4,25 @@ import {useFilter} from './useFilter'; import './ResourcesList.css'; -type ResourcesListProps = {resources: Resource[]; searchQuery: string}; +type ResourcesListProps = { + resources: Resource[]; + searchQuery: string; + sortAttribute: string; + allowedTypes: string[]; +}; -const ResourcesList = ({resources, searchQuery}: ResourcesListProps) => { - const filteredResources = useFilter(resources, searchQuery); +const ResourcesList = ({ + resources, + searchQuery, + sortAttribute, + allowedTypes, +}: ResourcesListProps) => { + const filteredResources = useFilter( + resources, + searchQuery, + sortAttribute, + allowedTypes + ); return (

{resources.length > 0 ? 'Found Resources' : 'No Resources Found'}

diff --git a/visualization_tool/src/components/ResourcesList/useFilter.ts b/visualization_tool/src/components/ResourcesList/useFilter.ts index 9f115b04..c3203e0e 100644 --- a/visualization_tool/src/components/ResourcesList/useFilter.ts +++ b/visualization_tool/src/components/ResourcesList/useFilter.ts @@ -2,7 +2,12 @@ import {useEffect, useState} from 'react'; import {Resource} from '../../types/resources'; import {debounce} from '@mui/material'; -export const useFilter = (resources: Resource[], searchQuery: string) => { +export const useFilter = ( + resources: Resource[], + searchQuery: string, + sortAttribute: string, + allowedTypes: string[] +) => { const [filteredResources, setFilteredResources] = useState(resources); @@ -11,21 +16,26 @@ export const useFilter = (resources: Resource[], searchQuery: string) => { setFilteredResources( resources .filter(resource => { - return resource.name - .toLowerCase() - .includes(searchQuery.toLowerCase()); - }) - .sort((a, b) => { return ( - new Date(a.creationTimestamp).getTime() - - new Date(b.creationTimestamp).getTime() + allowedTypes.includes(resource.type) && + resource.name.toLowerCase().includes(searchQuery.toLowerCase()) ); }) + .sort((a, b) => { + if (sortAttribute === 'name') { + return a.name.localeCompare(b.name); + } else { + return ( + new Date(a.creationTimestamp).getTime() - + new Date(b.creationTimestamp).getTime() + ); + } + }) ); }; debounce(filterResources, 100)(); - }, [resources, searchQuery]); + }, [resources, searchQuery, sortAttribute, allowedTypes]); return filteredResources; };