diff --git a/app/public/locales/en/common.yml b/app/public/locales/en/common.yml index 0f8ba305..e5b293d6 100644 --- a/app/public/locales/en/common.yml +++ b/app/public/locales/en/common.yml @@ -676,7 +676,12 @@ filters: root_relative_squared_error: "Root relative squared error" c_index: "C-index" f_measure: "F-measure" - + dependencies: "Libraries" + sklearn: "sklearn {{version}}" + torch: "Torch {{version}}" + Weka: "Weka {{version}}" + mlr: "MLR {{version}}" + Moa: "Moa {{version}}" diff --git a/app/src/components/navbar/NavbarSearch.js b/app/src/components/navbar/NavbarSearch.js index f14c7f98..7c63eae8 100644 --- a/app/src/components/navbar/NavbarSearch.js +++ b/app/src/components/navbar/NavbarSearch.js @@ -18,6 +18,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { Box, InputBase, MenuItem, Select } from "@mui/material"; import { SearchProvider, SearchBox } from "@elastic/react-search-ui"; +import { useTheme } from "@mui/system"; const SearchWrapper = styled(Box)` border-radius: 2px; @@ -150,7 +151,8 @@ const SearchBar = memo(() => { PaperProps: { sx: { boxShadow: 2, - border: "1px solid #d3d4d5", + border: `1px solid rgba(0, 0, 0, 0.12); + `, }, }, }} diff --git a/app/src/components/search/CoreFilter.js b/app/src/components/search/CoreFilter.js index b0fb5f8a..f51a2a40 100644 --- a/app/src/components/search/CoreFilter.js +++ b/app/src/components/search/CoreFilter.js @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { withSearch } from "@elastic/react-search-ui"; const CoreFilter = ({ addFilter, removeFilter, setSort }) => { diff --git a/app/src/components/search/Filter.js b/app/src/components/search/Filter.js index bd206c2d..6ffb38dc 100644 --- a/app/src/components/search/Filter.js +++ b/app/src/components/search/Filter.js @@ -1,24 +1,63 @@ -import React from "react"; - +import { Chip } from "@mui/material"; import styled from "@emotion/styled"; -import { Chip as MuiChip } from "@mui/material"; +import { i18n } from "next-i18next"; +import React from "react"; -const FilterChip = styled(MuiChip)` - margin-left: 10px; +const FilterChip = styled(Chip)` + margin-right: 10px; margin-top: 10px; margin-bottom: 10px; -`; -const FilterPanel = styled.div` - border-right: 1px solid rgba(0, 0, 0, 0.12); - border-bottom: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 50px; `; +// Handles special cases in the filter options +const processOption = (option) => { + // Homogenize notation for library reporting and versioning + const libraries = [ + "sklearn", + "torch", + "Weka", + "tensorflow", + "keras", + "mlr", + "Moa", + ]; + if (libraries.some((library) => option.includes(library))) { + const segments = option.split(/,|\n/); + let lib = segments.find((segment) => + libraries.some((library) => segment.includes(library)), + ); + if (lib.startsWith(" ")) { + lib = lib.replace(" ", ""); + } + if ( + lib.startsWith("Weka_") || + lib.startsWith("R_") || + lib.startsWith("Moa_") || + lib.startsWith("mlr_") + ) { + lib = lib.replace("_", "=="); + } + // If the option has versioning, return it separately + const values = lib.split("=="); + if (values.length > 1) { + return [`filters.${values[0]}`, { version: values[1] }]; + } else { + return [`filters.${lib}`]; + } + } else { + return [`filters.${option}`]; + } +}; + const Filter = ({ label, options, values, onRemove, onSelect }) => { return ( - + {options.map((option) => ( @@ -28,7 +67,7 @@ const Filter = ({ label, options, values, onRemove, onSelect }) => { variant={option.selected ? "default" : "outlined"} /> ))} - + ); }; diff --git a/app/src/components/search/ResultCard.js b/app/src/components/search/ResultCard.js index ed7fe2c9..e4b217b8 100644 --- a/app/src/components/search/ResultCard.js +++ b/app/src/components/search/ResultCard.js @@ -20,6 +20,11 @@ import { Description as TaskDescription, stats as taskStats, } from "../../pages/t/taskCard"; +import { + Title as FlowTitle, + Description as FlowDescription, + stats as flowStats, +} from "../../pages/f/flowCard"; import { faHashtag, faHistory } from "@fortawesome/free-solid-svg-icons"; @@ -106,18 +111,21 @@ const abbreviateNumber = (value) => { const titles = { data: DataTitle, task: TaskTitle, + flow: FlowTitle, // Add other mappings as needed }; const descriptions = { data: DataDescription, task: TaskDescription, + flow: FlowDescription, // Add other mappings as needed }; const statistics = { data: dataStats, task: taskStats, + flow: flowStats, // Add other mappings as needed }; diff --git a/app/src/components/search/SearchContainer.js b/app/src/components/search/SearchContainer.js index 82663ed2..e08e09e7 100644 --- a/app/src/components/search/SearchContainer.js +++ b/app/src/components/search/SearchContainer.js @@ -10,7 +10,6 @@ import { FormControl, InputLabel, Tab, - Chip, Tabs, Card, } from "@mui/material"; @@ -40,6 +39,7 @@ import { import Wrapper from "../Wrapper"; import { i18n } from "next-i18next"; import ResultsTable from "./ResultTable"; +import Filter from "./Filter"; import TagFilter from "./TagFilter"; const SearchResults = styled(Results)` @@ -178,61 +178,9 @@ const PagingView = ({ current, totalPages, onChange }) => ( /> ); -// Allows overriding the filter option text -const facet_aliases = { - Status: { - active: "verified", - in_preparation: "in preparation", - deactivated: "deprecated", - }, -}; - -const FilterChip = styled(Chip)` - margin-right: 10px; - margin-top: 10px; - margin-bottom: 10px; - border-radius: 50px; -`; - -const FilterPanel = styled.div``; - -const Filter = ({ label, options, values, onRemove, onSelect }) => { - return ( - - {options.map((option) => ( - - option.selected ? onRemove(option.value) : onSelect(option.value) - } - color={option.selected ? "primary" : "default"} - variant={option.selected ? "default" : "outlined"} - /> - ))} - - ); -}; - // This is the Search UI component. The config contains the search state and actions. const SearchContainer = memo( - ({ - config, - sort_options, - search_facets, - columns, - facet_aliases, - title, - type, - }) => { + ({ config, sort_options, search_facets, columns }) => { const [filter, setFilter] = React.useState("hide"); const handleFilterChange = (event, newFilter) => { console.log(filter, newFilter); @@ -287,6 +235,7 @@ const SearchContainer = memo( view={Filter} value={filter} index={index} + show={25} /> ))} diff --git a/app/src/pages/d/search.js b/app/src/pages/d/search.js index 7eb7c56a..8953e5c2 100644 --- a/app/src/pages/d/search.js +++ b/app/src/pages/d/search.js @@ -13,7 +13,6 @@ import { renderChips, } from "../../components/search/ResultTable"; -import Chip from "@mui/material/Chip"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, @@ -260,8 +259,6 @@ function DataSearchContainer() { sort_options={sort_options} search_facets={search_facets} columns={columns} - title="Datasets" - type="Dataset" /> ); } diff --git a/app/src/pages/d/searchConfig.js b/app/src/pages/d/searchConfig.js index eb718b6d..9ad9447f 100644 --- a/app/src/pages/d/searchConfig.js +++ b/app/src/pages/d/searchConfig.js @@ -70,7 +70,7 @@ const searchConfig = { facets: { "status.keyword": { type: "value" }, "name.keyword": { type: "value" }, - "licence.keyword": { type: "value" }, + "licence.keyword": { type: "value", size: 10 }, "qualities.NumberOfInstances": { type: "range", ranges: [ diff --git a/app/src/pages/f/flowCard.js b/app/src/pages/f/flowCard.js new file mode 100644 index 00000000..5ce78444 --- /dev/null +++ b/app/src/pages/f/flowCard.js @@ -0,0 +1,67 @@ +import { Box } from "@mui/material"; +import React from "react"; +import { blue, green, orange, purple, red } from "@mui/material/colors"; +import { + faCheck, + faCloudDownloadAlt, + faFlask, + faHeart, + faTimes, + faWrench, +} from "@fortawesome/free-solid-svg-icons"; +import Teaser from "../../components/search/Teaser"; + +const status = { + active: { + title: "verified", + icon: faCheck, + color: green[500], + }, + deactivated: { + title: "deactivated", + icon: faTimes, + color: red[500], + }, + in_preparation: { + title: "unverified", + icon: faWrench, + color: orange[500], + }, +}; + +export const Title = ({ result }) => { + return ( + + {result.name.raw} + + ); +}; + +export const stats = [ + { param: "runs.raw", unit: "runs", color: red[500], icon: faFlask }, + { + param: "nr_of_likes.raw", + unit: "likes", + color: purple[500], + icon: faHeart, + }, + { + param: "nr_of_downloads.raw", + unit: "downloads", + color: blue[500], + icon: faCloudDownloadAlt, + }, +]; + +export const Description = ({ result }) => { + return ( + + ); +}; diff --git a/app/src/pages/f/search.js b/app/src/pages/f/search.js new file mode 100644 index 00000000..10769c17 --- /dev/null +++ b/app/src/pages/f/search.js @@ -0,0 +1,189 @@ +import React from "react"; +import { useNextRouting } from "../../utils/useNextRouting"; + +import DashboardLayout from "../../layouts/Dashboard"; +import SearchContainer from "../../components/search/SearchContainer"; +import { + renderCell, + valueGetter, + renderDescription, + renderDate, + renderTags, + renderChips, +} from "../../components/search/ResultTable"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import searchConfig from "../f/searchConfig"; +import { + faCreativeCommonsBy, + faCreativeCommonsPd, + faCreativeCommonsZero, +} from "@fortawesome/free-brands-svg-icons"; + +// Server-side translation +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +export async function getStaticProps(context) { + // extract the locale identifier from the URL + const { locale } = context; + return { + props: { + // pass the translation props to the page component + ...(await serverSideTranslations(locale)), + }, + }; +} + +// Defines chips in table view +const getChipProps = (value) => { + switch (value) { + // Dataset licence + case "public": + case "Public": + return { + label: "Public", + icon: , + color: "success", + }; + case "CC0": + case "CCZero": + case "CC0: Public Domain": + case "Public Domain (CC0)": + return { + label: "CC0", + icon: , + color: "success", + }; + case "CC BY 4.0": + case "CC BY-NC-ND": + case "CC BY-NC 4.0": + case "CC BY-SA": + case "CC BY": + case "Creative Commons Attribution": + return { + label: value, + icon: , + color: "primary", + }; + case "Open Database License (ODbL)": + return { + label: "ODbL", + icon: , + color: "success", + }; + default: + return { + label: value, + }; + } +}; + +const sort_options = [ + { + name: "search.relevance", + value: [], + }, + { + name: "search.most_runs", + value: [{ field: "runs", direction: "desc" }], + }, + { + name: "search.most_likes", + value: [{ field: "nr_of_likes", direction: "desc" }], + }, + { + name: "search.most_downloads", + value: [{ field: "nr_of_downloads", direction: "desc" }], + }, + { + name: "search.most_recent", + value: [{ field: "date", direction: "desc" }], + }, +]; + +const search_facets = [ + { + label: "filters.dependencies", + field: "dependencies.keyword", + }, +]; + +// Controls how columns are rendered and manipulated in the table view +const columns = [ + { + field: "flow_id", + headerName: "Flow_id", + valueGetter: valueGetter("data_id"), + renderCell: renderCell, + width: 70, + }, + { + field: "name", + headerName: "Name", + valueGetter: valueGetter("name"), + renderCell: renderCell, + width: 230, + }, + { + field: "version", + headerName: "Version", + valueGetter: valueGetter("version"), + renderCell: renderCell, + width: 60, + }, + { + field: "description", + headerName: "Description", + valueGetter: valueGetter("description"), + renderCell: renderDescription, + width: 360, + }, + { + field: "date", + headerName: "Date", + valueGetter: valueGetter("date"), + renderCell: renderDate, + }, + { + field: "licence", + headerName: "Licence", + valueGetter: valueGetter("licence"), + renderCell: renderChips(getChipProps), + width: 110, + }, + { + field: "creator", + headerName: "Creator", + valueGetter: valueGetter("creator"), + renderCell: renderCell, + width: 150, + }, + { + field: "tags", + headerName: "Tags", + valueGetter: valueGetter("tags"), + renderCell: renderTags, + width: 400, + }, +]; + +function DataSearchContainer() { + const combinedConfig = useNextRouting(searchConfig, ""); + + return ( + + ); +} + +DataSearchContainer.getLayout = function getLayout(page) { + return {page}; +}; + +DataSearchContainer.displayName = "DataSearchContainer"; + +export default DataSearchContainer; diff --git a/app/src/pages/f/searchConfig.js b/app/src/pages/f/searchConfig.js new file mode 100644 index 00000000..49dee85d --- /dev/null +++ b/app/src/pages/f/searchConfig.js @@ -0,0 +1,91 @@ +import Connector from "../../services/SearchAPIConnector"; +const apiConnector = new Connector("flow"); + +const searchConfig = { + apiConnector: apiConnector, + alwaysSearchOnInitialLoad: true, + searchQuery: { + resultsPerPage: 100, + search_fields: { + name: { weight: 3 }, + exact_name: { weight: 3 }, + description: { weight: 3 }, + full_description: { weight: 3 }, + "tags.tag": { weight: 3 }, + "parameters.full_name": { weight: 3 }, + "parameters.description": { weight: 3 }, + uploader: { weight: 2 }, + installation_notes: { weight: 1 }, + dependencies: { weight: 1 }, + licence: { weight: 1 }, + }, + result_fields: { + contributor: { raw: {} }, + creator: { raw: {} }, + flow_id: { raw: {} }, + date: { raw: {} }, + name: { + snippet: { + size: 100, + fallback: true, + }, + }, + description: { + snippet: { + size: 100, + fallback: true, + }, + }, + full_description: { + snippet: { + size: 100, + fallback: true, + }, + }, + installation_notes: { + snippet: { + size: 100, + fallback: true, + }, + }, + licence: { raw: {} }, + nr_of_likes: { raw: {} }, + nr_of_downloads: { raw: {} }, + runs: { raw: {} }, + status: { raw: {} }, + tags: { raw: {} }, + total_downloads: { raw: {} }, + uploader: { raw: {} }, + uploader_id: { raw: {} }, + url: { raw: {} }, + visibility: { raw: {} }, + dependencies: { raw: {} }, + version: { raw: {} }, + suggest: { raw: {} }, + }, + disjunctiveFacets: ["dependencies"], + facets: { + "dependencies.keyword": { type: "value", size: 50 }, + }, + group: { + //This doesn't work yet. TODO: figure out how to group. + field: { name: { raw: {} } }, + }, + }, + autocompleteQuery: { + results: { + resultsPerPage: 100, + result_fields: { + // specify the fields you want from the index to display the results + name: { snippet: { size: 100, fallback: true } }, + url: { raw: {} }, + }, + search_fields: { + // specify the fields you want to search on + name: {}, + }, + }, + }, +}; + +export default searchConfig; diff --git a/app/src/pages/t/search.js b/app/src/pages/t/search.js index 04e1e0aa..8038cdc2 100644 --- a/app/src/pages/t/search.js +++ b/app/src/pages/t/search.js @@ -78,8 +78,6 @@ function TaskSearchContainer() { sort_options={sort_options} search_facets={search_facets} columns={columns} - title="Tasks" - type="Task" /> ); } diff --git a/app/src/services/SearchAPIConnector.js b/app/src/services/SearchAPIConnector.js index 65680905..fd6da433 100644 --- a/app/src/services/SearchAPIConnector.js +++ b/app/src/services/SearchAPIConnector.js @@ -26,6 +26,7 @@ class SearchAPIConnector { }; //console.log(request.body); const response = await fetch("/api/search", request); + //console.log(response); return response.json(); }