global search implementation (#88)

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2022-09-30 18:51:54 +03:00 committed by GitHub
parent 9cbf029786
commit e18279a32c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 366 additions and 227 deletions

45
package-lock.json generated
View File

@ -19,6 +19,8 @@
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.24.0",
"downshift": "^6.1.12",
"lodash": "^4.17.21",
"luxon": "^2.4.0",
"npm": "^8.11.0",
"nth-check": "^2.0.1",
@ -6611,6 +6613,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"node_modules/compute-scroll-into-view": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz",
"integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg=="
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -7677,6 +7684,21 @@
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
"dev": true
},
"node_modules/downshift": {
"version": "6.1.12",
"resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz",
"integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==",
"dependencies": {
"@babel/runtime": "^7.14.8",
"compute-scroll-into-view": "^1.0.17",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"tslib": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.12.0"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -19662,8 +19684,7 @@
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@ -25638,6 +25659,11 @@
}
}
},
"compute-scroll-into-view": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz",
"integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -26430,6 +26456,18 @@
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
"dev": true
},
"downshift": {
"version": "6.1.12",
"resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz",
"integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==",
"requires": {
"@babel/runtime": "^7.14.8",
"compute-scroll-into-view": "^1.0.17",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"tslib": "^2.3.0"
}
},
"duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -35103,8 +35141,7 @@
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"tsutils": {
"version": "3.21.0",

View File

@ -14,6 +14,8 @@
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.24.0",
"downshift": "^6.1.12",
"lodash": "^4.17.21",
"luxon": "^2.4.0",
"npm": "^8.11.0",
"nth-check": "^2.0.1",

View File

@ -16,7 +16,6 @@ function App() {
return localStorageToken ? true : false;
};
const [searchKeywords, setSearchKeywords] = useState(null);
const [data, setData] = useState(null);
const [isAuthEnabled, setIsAuthEnabled] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(isToken());
@ -27,30 +26,10 @@ function App() {
<Routes>
<Route element={<AuthWrapper isLoggedIn={isLoggedIn} redirect="/login" />}>
<Route path="/" element={<Navigate to="/home" />} />
<Route
path="/home"
element={
<HomePage
keywords={searchKeywords}
updateKeywords={setSearchKeywords}
data={data}
updateData={setData}
/>
}
/>
<Route
path="/explore"
element={
<ExplorePage
keywords={searchKeywords}
updateKeywords={setSearchKeywords}
data={data}
updateData={setData}
/>
}
/>
<Route path="/image/:name" element={<RepoPage />} />
<Route path="/image/:name/tag/:tag" element={<TagPage />} />
<Route path="/home" element={<HomePage data={data} updateData={setData} />} />
<Route path="/explore" element={<ExplorePage data={data} updateData={setData} />} />
<Route path="/image/:name" element={<RepoPage updateData={setData} />} />
<Route path="/image/:name/tag/:tag" element={<TagPage updateData={setData} />} />
</Route>
<Route element={<AuthWrapper isLoggedIn={!isLoggedIn} redirect="/" />}>
<Route

View File

@ -2,8 +2,9 @@ import { render, screen, waitFor } from '@testing-library/react';
import { api } from 'api';
import Explore from 'components/Explore';
import React, { useState } from 'react';
import { MemoryRouter } from 'react-router-dom';
// useNavigate mock
// router mock
const mockedUsedNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
// @ts-ignore
@ -11,45 +12,50 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedUsedNavigate
}));
const StateExploreWrapper = () => {
const StateExploreWrapper = (props) => {
const [data, useData] = useState([]);
return <Explore data={data} keywords={''} updateData={useData} />;
const queryString = props.search || '';
return (
<MemoryRouter initialEntries={[queryString]}>
<Explore data={data} updateData={useData} />
</MemoryRouter>
);
};
const mockImageList = {
RepoListWithNewestImage: [
{
Name: 'alpine',
Size: '2806985',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
NewestImage: {
RepoName: 'alpine',
Tag: 'latest',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
Description: 'w',
Licenses: '',
Vendor: '',
Size: '2806985',
Labels: ''
}
},
{
Name: 'mongo',
Size: '231383863',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
NewestImage: {
RepoName: 'mongo',
Tag: 'latest',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
Description: '',
Licenses: '',
Vendor: '',
Size: '231383863',
Labels: ''
}
},
{
Name: 'nodeUnique',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
NewestImage: {
RepoName: 'nodeUnique',
Tag: 'latest',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
Description: '',
Licenses: '',
Vendor: '',
Size: '369311301',
Labels: ''
}
}

View File

@ -7,10 +7,7 @@ it('renders the explore page component', () => {
render(
<BrowserRouter>
<Routes>
<Route
path="*"
element={<ExplorePage data={[]} keywords={''} updateData={() => {}} updateKeywords={() => {}} />}
/>
<Route path="*" element={<ExplorePage data={[]} updateData={() => {}} />} />
</Routes>
</BrowserRouter>
);

View File

@ -13,43 +13,43 @@ jest.mock('react-router-dom', () => ({
const StateHomeWrapper = () => {
const [data, useData] = useState([]);
return <Home data={data} keywords={''} updateData={useData} />;
return <Home data={data} updateData={useData} />;
};
const mockImageList = {
RepoListWithNewestImage: [
{
Name: 'alpine',
Size: '2806985',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
NewestImage: {
RepoName: 'alpine',
Tag: 'latest',
LastUpdated: '2022-08-09T17:19:53.274069586Z',
Description: '',
Description: 'w',
Licenses: '',
Vendor: '',
Size: '2806985',
Labels: ''
}
},
{
Name: 'mongo',
Size: '231383863',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
NewestImage: {
RepoName: 'mongo',
Tag: 'latest',
LastUpdated: '2022-08-02T01:30:49.193203152Z',
Description: '',
Licenses: '',
Vendor: '',
Size: '231383863',
Labels: ''
}
},
{
Name: 'node',
Size: '369311301',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
NewestImage: {
RepoName: 'node',
Tag: 'latest',
LastUpdated: '2022-08-23T00:20:40.144281895Z',
Description: '',
Licenses: '',
Vendor: '',
Size: '369311301',
Labels: ''
}
}

View File

@ -7,10 +7,7 @@ it('renders the homepage component', () => {
render(
<BrowserRouter>
<Routes>
<Route
path="*"
element={<HomePage data={[]} keywords={'test'} updateData={() => {}} updateKeywords={() => {}} />}
/>
<Route path="*" element={<HomePage data={[]} updateData={() => {}} />} />
</Routes>
</BrowserRouter>
);

View File

@ -21,7 +21,7 @@ it('renders the repository page component', () => {
render(
<BrowserRouter>
<Routes>
<Route path="*" element={<RepoPage updateKeywords={() => {}} />} />
<Route path="*" element={<RepoPage />} />
</Routes>
</BrowserRouter>
);

View File

@ -20,7 +20,7 @@ it('renders the tags page component', async () => {
render(
<BrowserRouter>
<Routes>
<Route path="*" element={<TagPage updateKeywords={() => {}} />} />
<Route path="*" element={<TagPage updateData={() => {}} />} />
</Routes>
</BrowserRouter>
);

View File

@ -59,15 +59,21 @@ const api = {
};
const endpoints = {
imageList:
'/v2/_zot/ext/search?query={RepoListWithNewestImage(){Platforms {Os Arch} NewestImage {RepoName Tag LastUpdated Description Licenses Vendor Size Labels} }}',
repoList:
'/v2/_zot/ext/search?query={RepoListWithNewestImage(){Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Description Licenses Title Source Documentation History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}} Vendor Labels} DownloadCount}}',
detailedRepoInfo: (name) =>
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag Layers {Size Digest}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName Layers {Size Digest} Digest Tag Title Documentation DownloadCount Source Description History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}}}}}}`,
vulnerabilitiesForRepo: (name) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
globalSearch: (searchQuery) =>
`/v2/_zot/ext/search?query={GlobalSearch(query:"${searchQuery}") {Repos {Name LastUpdated Size Platforms { Os Arch } NewestImage { Tag Description Licenses Vendor Labels } DownloadCount}}}`,
layersDetailsForImage: (name) =>
`/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`,
dependsOnForImage: (name) => `/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName}}`
dependsOnForImage: (name) => `/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName}}`,
globalSearchPaginated: (searchQuery, pageNumber, pageSize) =>
`/v2/_zot/ext/search?query={GlobalSearch(query:"${searchQuery}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize
}}) {Repos {Name LastUpdated Size Platforms { Os Arch } NewestImage { Tag Description Licenses Vendor Labels } DownloadCount}}}`
};
export { api, endpoints };

View File

@ -13,8 +13,8 @@ import makeStyles from '@mui/styles/makeStyles';
// utility
import { api, endpoints } from '../api';
import { host } from '../host';
import { isEmpty } from 'lodash';
//
import { mapToRepo } from 'utilities/objectModels.js';
import { useSearchParams } from 'react-router-dom';
const useStyles = makeStyles(() => ({
gridWrapper: {
@ -44,64 +44,46 @@ const useStyles = makeStyles(() => ({
}
}));
function Explore({ keywords, data, updateData }) {
function Explore({ data, updateData }) {
const [isLoading, setIsLoading] = useState(true);
const [filteredData, setFilteredData] = useState([]);
const [exploreData, setExploreData] = useState([]);
// const [sortFilter, setSortFilter] = useState('');
const filterStr = keywords && keywords.toLocaleLowerCase();
const [queryParams] = useSearchParams();
const classes = useStyles();
useEffect(() => {
api
.get(`${host()}${endpoints.imageList}`)
.then((response) => {
if (response.data && response.data.data) {
let imageList = response.data.data.RepoListWithNewestImage;
let imagesData = imageList.map((image) => {
return {
name: image.NewestImage.RepoName,
latestVersion: image.NewestImage.Tag,
tags: image.NewestImage.Labels,
description: image.NewestImage.Description,
platforms: image.Platforms,
licenses: image.NewestImage.Licenses,
size: image.NewestImage.Size,
vendor: image.NewestImage.Vendor,
lastUpdated: image.NewestImage.LastUpdated
};
});
updateData(imagesData);
setIsLoading(false);
}
})
.catch((e) => {
console.error(e);
});
}, []);
if (!queryParams.get('search')) {
api
.get(`${host()}${endpoints.repoList}`)
.then((response) => {
if (response.data && response.data.data) {
let repoList = response.data.data.RepoListWithNewestImage;
let repoData = repoList.map((responseRepo) => {
return mapToRepo(responseRepo);
});
updateData(repoData);
setIsLoading(false);
}
})
.catch((e) => {
console.error(e);
});
}
}, [updateData, queryParams]);
useEffect(() => {
setIsLoading(false);
}, []);
// Update component data based on response data, here filtering logic will be added
useEffect(() => {
const filtered =
data &&
data.filter((item) => {
return (
isEmpty(keywords) ||
(item.name && item.name.toLocaleLowerCase().indexOf(filterStr) >= 0) ||
(item.appID && item.appID.toLocaleLowerCase().indexOf(filterStr) >= 0) ||
(item.appId && item.appId.toLocaleLowerCase().indexOf(filterStr) >= 0)
);
});
setFilteredData(filtered);
}, [keywords, filterStr, data]);
setExploreData(data);
}, [data]);
const renderRepoCards = () => {
return (
filteredData &&
filteredData.map((item, index) => {
exploreData &&
exploreData.map((item, index) => {
return (
<RepoCard
name={item.name}
@ -143,7 +125,7 @@ function Explore({ keywords, data, updateData }) {
return (
<Container maxWidth="lg">
{isLoading && <Loading />}
{!(filteredData && filteredData.length) ? (
{!(exploreData && exploreData.length) ? (
<Grid container className={classes.nodataWrapper}>
<div style={{ marginTop: 20 }}>
<div style={{}}>
@ -160,7 +142,7 @@ function Explore({ keywords, data, updateData }) {
<Grid item xs={12}>
<Stack direction="row" className={classes.resultsRow}>
<Typography variant="body2" className={classes.results}>
Results {filteredData.length}
Results {exploreData.length}
</Typography>
{/* <FormControl sx={{m:'1', minWidth:"4.6875rem"}} className={classes.sortForm} size="small">
<InputLabel>Sort</InputLabel>

View File

@ -1,12 +1,11 @@
// react global
import React from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
// components
import {
AppBar,
Toolbar,
InputBase,
Popper,
MenuList,
MenuItem,
@ -16,13 +15,14 @@ import {
Stack,
IconButton
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import Avatar from '@mui/material/Avatar';
// styling
import makeStyles from '@mui/styles/makeStyles';
import logo from '../assets/Zot-white-text.svg';
import placeholderProfileButton from '../assets/Profile_button_placeholder.svg';
import { useState, useRef } from 'react';
import SearchSuggestion from './SearchSuggestion';
const useStyles = makeStyles(() => ({
header: {
@ -37,24 +37,14 @@ const useStyles = makeStyles(() => ({
borderBottom: '0.0625rem solid #BDBDBD',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)'
},
search: {
display: 'flex',
alignItems: 'center',
backgroundColor: '#FFFFFF',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
borderRadius: '2.5rem',
border: '0.125rem solid #E7E7E7',
minWidth: '60%',
marginLeft: 16,
flexDirection: 'row'
},
searchIcon: {
color: '#52637A',
paddingRight: '3%'
},
input: {
color: '#464141',
marginLeft: 1
marginLeft: 1,
width: '90%'
},
icons: {
@ -78,13 +68,13 @@ const useStyles = makeStyles(() => ({
}
}));
function Header() {
function Header({ updateData }) {
const classes = useStyles();
const path = useLocation().pathname;
const navigate = useNavigate();
// const navigate = useNavigate();
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
@ -100,10 +90,6 @@ function Header() {
setOpen(false);
};
const goToExplore = () => {
navigate(`/explore`);
};
return (
<AppBar sx={{ position: 'sticky', minHeight: '10%' }}>
<Toolbar className={classes.header}>
@ -111,23 +97,7 @@ function Header() {
<Link to="/home" className={classes.logoWrapper}>
<Avatar alt="zot" src={logo} className={classes.logo} variant="square" />
</Link>
{path !== '/' && (
<Stack
className={classes.search}
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={2}
>
<InputBase
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
placeholder="Search for content..."
className={classes.input}
onKeyDown={() => goToExplore()}
/>
<SearchIcon className={classes.searchIcon} />
</Stack>
)}
{path !== '/' && <SearchSuggestion updateData={updateData} />}
<IconButton
ref={anchorRef}
id="composition-button"

View File

@ -2,10 +2,10 @@ import { Grid, Stack, Typography } from '@mui/material';
import { makeStyles } from '@mui/styles';
import { api, endpoints } from 'api';
import { host } from '../host';
import { isEmpty } from 'lodash';
import React, { useEffect, useState } from 'react';
import PreviewCard from './PreviewCard';
import RepoCard from './RepoCard';
import { mapToRepo } from 'utilities/objectModels';
const useStyles = makeStyles(() => ({
gridWrapper: {
@ -59,58 +59,32 @@ const useStyles = makeStyles(() => ({
}
}));
function Home({ keywords, data, updateData }) {
function Home({ data, updateData }) {
// const [isLoading, setIsLoading] = useState(true);
const [homeData, setHomeData] = useState([]);
const filterStr = keywords && keywords.toLocaleLowerCase();
const classes = useStyles();
useEffect(() => {
api
.get(`${host()}${endpoints.imageList}`)
.get(`${host()}${endpoints.repoList}`)
.then((response) => {
if (response.data && response.data.data) {
let imageList = response.data.data.RepoListWithNewestImage;
let imagesData = imageList.map((image) => {
return {
name: image.NewestImage.RepoName,
latestVersion: image.NewestImage.Tag,
tags: image.NewestImage.Labels,
description: image.NewestImage.Description,
platforms: image.Platforms,
licenses: image.NewestImage.Licenses,
size: image.NewestImage.Size,
vendor: image.NewestImage.Vendor,
lastUpdated: image.NewestImage.LastUpdated
};
let repoList = response.data.data.RepoListWithNewestImage;
let repoData = repoList.map((responseRepo) => {
return mapToRepo(responseRepo);
});
updateData(imagesData);
updateData(repoData);
// setIsLoading(false);
}
})
.catch((e) => {
console.error(e);
});
}, []);
}, [updateData]);
useEffect(() => {
// setIsLoading(false);
}, []);
useEffect(() => {
const filtered =
data &&
data.filter((item) => {
return (
isEmpty(keywords) ||
(item.name && item.name.toLocaleLowerCase().indexOf(filterStr) >= 0) ||
(item.appID && item.appID.toLocaleLowerCase().indexOf(filterStr) >= 0) ||
(item.appId && item.appId.toLocaleLowerCase().indexOf(filterStr) >= 0)
);
});
setHomeData(filtered);
}, [keywords, data]);
setHomeData(data);
}, [data]);
const renderPreviewCards = () => {
return (

View File

@ -1,24 +0,0 @@
// components
import { Container } from '@mui/material';
import Explore from './Explore.jsx';
import makeStyles from '@mui/styles/makeStyles';
import React from 'react';
const useStyles = makeStyles(() => ({
container: {
padding: 5
}
}));
function Rightbar({ data, keywords, updateData }) {
const classes = useStyles();
return (
<Container className={classes.container}>
<Explore keywords={keywords} data={data} updateData={updateData} />
</Container>
);
}
export default Rightbar;

View File

@ -0,0 +1,198 @@
import { InputBase, List, ListItem, Stack, Typography } from '@mui/material';
import { makeStyles } from '@mui/styles';
import PhotoIcon from '@mui/icons-material/Photo';
import SearchIcon from '@mui/icons-material/Search';
import React, { useEffect, useMemo, useState } from 'react';
import { api, endpoints } from 'api';
import { host } from 'host';
import { mapToRepo } from 'utilities/objectModels';
import { createSearchParams, useNavigate } from 'react-router-dom';
import { debounce } from 'lodash';
import { useCombobox } from 'downshift';
const useStyles = makeStyles(() => ({
searchContainer: {
display: 'inline-block',
backgroundColor: '#FFFFFF',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
minWidth: '60%',
marginLeft: 16,
position: 'relative',
zIndex: 1150
},
search: {
position: 'relative',
minWidth: '100%',
flexDirection: 'row',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
border: '0.125rem solid #E7E7E7',
borderRadius: '2.5rem',
zIndex: 1155
},
resultsWrapper: {
margin: '0',
marginTop: '-2%',
paddingTop: '3%',
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#FFFFFF',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
borderBottomLeftRadius: '2.5rem',
borderBottomRightRadius: '2.5rem',
// border: '0.125rem solid #E7E7E7',
borderTop: 0,
width: '100%',
overflowY: 'auto',
zIndex: 1
},
resultsWrapperHidden: {
display: 'none'
},
searchIcon: {
color: '#52637A',
paddingRight: '3%',
cursor: 'pointer'
},
input: {
color: '#464141',
marginLeft: 1,
width: '90%'
},
searchItem: {
alignItems: 'center',
justifyContent: 'flex-start',
color: '#000000',
height: '2.75rem',
padding: '0 5%'
},
searchItemIcon: {
color: '#0000008A'
}
}));
function SearchSuggestion({ updateData }) {
const [searchQuery, setSearchQuery] = useState('');
const [suggestionData, setSuggestionData] = useState([]);
const navigate = useNavigate();
const classes = useStyles();
const handleSuggestionSelected = (event) => {
const name = event.selectedItem?.name;
navigate(`/image/${encodeURIComponent(name)}`);
};
const handleSearch = (event) => {
const { key, type } = event;
if (key === 'Enter' || type === 'click') {
api
.get(`${host()}${endpoints.globalSearch(searchQuery)}`)
.then((response) => {
if (response.data && response.data.data) {
let repoList = response.data.data.GlobalSearch.Repos;
let repoData = repoList.map((responseRepo) => {
return mapToRepo(responseRepo);
});
updateData(repoData);
navigate({ pathname: `/explore`, search: createSearchParams({ search: searchQuery }).toString() });
}
})
.catch((e) => {
console.error(e);
});
}
};
const handleSeachChange = (event) => {
const value = event.inputValue;
setSearchQuery(value);
if (value !== '' && value.length > 1) {
api
.get(`${host()}${endpoints.globalSearchPaginated(value, 1, 9)}`)
.then((suggestionResponse) => {
if (suggestionResponse.data.data.GlobalSearch.Repos) {
const suggestionParsedData = suggestionResponse.data.data.GlobalSearch.Repos.map((el) => mapToRepo(el));
setSuggestionData(suggestionParsedData);
}
})
.catch((e) => {
console.error(e);
});
}
};
const debounceSuggestions = useMemo(() => {
return debounce(handleSeachChange, 300);
}, []);
useEffect(() => {
return () => {
debounceSuggestions.cancel();
};
});
const {
// selectedItem,
getInputProps,
getMenuProps,
getItemProps,
highlightedIndex,
getComboboxProps,
isOpen
// closeMenu
} = useCombobox({
items: suggestionData,
onInputValueChange: debounceSuggestions,
onSelectedItemChange: handleSuggestionSelected,
itemToString: (item) => item.name ?? ''
});
const renderSuggestions = () => {
return suggestionData.map((suggestion, index) => (
<ListItem
key={`${suggestion.name}_${index}`}
className={classes.searchItem}
style={highlightedIndex === index ? { backgroundColor: '#F6F7F9' } : {}}
{...getItemProps({ item: suggestion, index })}
spacing={2}
>
<Stack direction="row" spacing={2}>
<PhotoIcon className={classes.searchItemIcon} />
<Typography>{suggestion.name}</Typography>
</Stack>
</ListItem>
));
};
return (
<div className={classes.searchContainer}>
<Stack
className={classes.search}
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={2}
{...getComboboxProps()}
>
<InputBase
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
placeholder="Search for content..."
className={classes.input}
onKeyUp={handleSearch}
{...getInputProps()}
/>
<div onClick={handleSearch} className={classes.searchIcon}>
<SearchIcon />
</div>
</Stack>
<List
{...getMenuProps()}
className={isOpen && suggestionData?.length > 0 ? classes.resultsWrapper : classes.resultsWrapperHidden}
>
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
</List>
</div>
);
}
export default SearchSuggestion;

View File

@ -140,7 +140,7 @@ export default function SignIn({ isAuthEnabled, setIsAuthEnabled, isLoggedIn, se
};
}
api
.get(`${host()}${endpoints.imageList}`, cfg)
.get(`${host()}${endpoints.repoList}`, cfg)
.then((response) => {
if (response.data && response.data.data) {
if (isAuthEnabled) {

View File

@ -1,10 +1,10 @@
// components
import React from 'react';
import Header from '../components/Header.jsx';
import Rightbar from '../components/Rightbar.jsx';
import makeStyles from '@mui/styles/makeStyles';
import { Container, Grid, Stack } from '@mui/material';
import Explore from 'components/Explore.jsx';
const useStyles = makeStyles(() => ({
container: {
@ -14,27 +14,27 @@ const useStyles = makeStyles(() => ({
minWidth: '60%'
},
gridWrapper: {
// backgroundColor: "#fff",
border: '0.0625rem #f2f2f2 dashed'
},
pageWrapper: {
height: '100%'
},
tile: {
width: '100%'
width: '100%',
padding: 5
}
}));
function ExplorePage({ data, updateData, keywords, updateKeywords }) {
function ExplorePage({ data, updateData }) {
const classes = useStyles();
return (
<Stack className={classes.pageWrapper} direction="column" data-testid="explore-container">
<Header updateKeywords={updateKeywords}></Header>
<Header updateData={updateData} />
<Container className={classes.container}>
<Grid container className={classes.gridWrapper}>
<Grid item className={classes.tile}>
<Rightbar keywords={keywords} data={data} updateData={updateData} />
<Explore data={data} updateData={updateData} />
</Grid>
</Grid>
</Container>

View File

@ -25,16 +25,16 @@ const useStyles = makeStyles(() => ({
}
}));
function HomePage({ data, updateData, keywords, updateKeywords }) {
function HomePage({ data, updateData }) {
const classes = useStyles();
return (
<Stack className={classes.pageWrapper} direction="column" data-testid="homepage-container">
<Header updateKeywords={updateKeywords}></Header>
<Header updateData={updateData} />
<Container className={classes.container}>
<Grid container className={classes.gridWrapper}>
<Grid item className={classes.tile}>
<Home keywords={keywords} data={data} updateData={updateData} />
<Home data={data} updateData={updateData} />
</Grid>
</Grid>
</Container>

View File

@ -29,12 +29,12 @@ const useStyles = makeStyles(() => ({
}
}));
function RepoPage(props) {
function RepoPage({ updateData }) {
const classes = useStyles();
return (
<Stack direction="column" className={classes.pageWrapper} data-testid="repo-container">
<Header updateKeywords={props.updateKeywords}></Header>
<Header updateData={updateData} />
<Container className={classes.container}>
<ExploreHeader />
<Grid container className={classes.gridWrapper}>

View File

@ -29,12 +29,12 @@ const useStyles = makeStyles(() => ({
}
}));
function TagPage(props) {
function TagPage({ updateData }) {
const classes = useStyles();
return (
<Stack direction="column" className={classes.pageWrapper} data-testid="tag-container">
<Header updateKeywords={props.updateKeywords}></Header>
<Header updateData={updateData} />
<Container className={classes.container}>
<ExploreHeader />
<Grid container className={classes.gridWrapper}>

View File

@ -0,0 +1,15 @@
const mapToRepo = (responseRepo) => {
return {
name: responseRepo.Name,
latestVersion: responseRepo.NewestImage?.Tag,
tags: responseRepo.NewestImage?.Labels,
description: responseRepo.NewestImage?.Description,
platforms: responseRepo.Platforms,
licenses: responseRepo.NewestImage?.Licenses,
size: responseRepo.Size,
vendor: responseRepo.NewestImage?.Vendor,
lastUpdated: responseRepo.LastUpdated
};
};
export { mapToRepo };