global search implementation (#88)
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
9cbf029786
commit
e18279a32c
45
package-lock.json
generated
45
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
29
src/App.js
29
src/App.js
@ -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
|
||||
|
@ -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: ''
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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: ''
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ it('renders the repository page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<RepoPage updateKeywords={() => {}} />} />
|
||||
<Route path="*" element={<RepoPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
12
src/api.js
12
src/api.js
@ -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 };
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
198
src/components/SearchSuggestion.jsx
Normal file
198
src/components/SearchSuggestion.jsx
Normal 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;
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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}>
|
||||
|
15
src/utilities/objectModels.js
Normal file
15
src/utilities/objectModels.js
Normal 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 };
|
Loading…
Reference in New Issue
Block a user