From f2f4f2534afd588bb170f4f9ea342cd7887fee65 Mon Sep 17 00:00:00 2001 From: Vincent T Date: Tue, 28 May 2024 13:15:56 -0400 Subject: [PATCH] frontend: Adds filter feature for app catalog Signed-off-by: Vincent T --- app-catalog/src/api/charts.tsx | 145 +++++++++++++++-- app-catalog/src/components/charts/List.tsx | 172 ++++++++++++++++----- app-catalog/src/index.tsx | 5 +- app-catalog/src/settings.tsx | 135 ++++++++++++++++ 4 files changed, 411 insertions(+), 46 deletions(-) create mode 100644 app-catalog/src/settings.tsx diff --git a/app-catalog/src/api/charts.tsx b/app-catalog/src/api/charts.tsx index 171cef3..54e8a69 100644 --- a/app-catalog/src/api/charts.tsx +++ b/app-catalog/src/api/charts.tsx @@ -1,22 +1,46 @@ import { PAGE_OFFSET_COUNT_FOR_CHARTS } from '../components/charts/List'; -export function fetchChartsFromArtifact( +export async function fetchChartsFromArtifact( search: string = '', + verified: boolean, + cncf: boolean, + loadNonMatchingCharts: boolean, category: { title: string; value: number }, page: number, limit: number = PAGE_OFFSET_COUNT_FOR_CHARTS ) { + // note: we are currently defaulting to search by verified and official as default + + // if we navigate to the last page we make a request to filter by false? + + // function to filter the seen charts + + // this array will be made of charts sourced from "response.packages" + + if (!category || category.value === 0) { - return fetch( - `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&sort=relevance&facets=true&limit=${limit}&offset=${ - (page - 1) * limit - }` - ).then(response => response.json()); + if (cncf) { + const dataResponse = await fetch( + `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&sort=relevance&facets=true&limit=${limit}&offset=${(page - 1) * limit}&verified_publisher=${verified}&cncf=${cncf}` + ).then(response => response.json()); + + console.log("dataResponse", dataResponse) + + + return fetch( + `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&sort=relevance&facets=true&limit=${limit}&offset=${(page - 1) * limit}&verified_publisher=${verified}&cncf=${cncf}` + ).then(response => response.json()); + } else { + return fetch( + `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&sort=relevance&facets=true&limit=${limit}&offset=${(page - 1) * limit}&verified_publisher=${verified}` + ).then(response => response.json()); + } } + return fetch( - `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&category=${ - category.value - }&sort=relevance&facets=true&limit=${limit}&offset=${(page - 1) * limit}` + `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&category=${category.value + }&sort=relevance&facets=true&limit=${limit}&offset=${(page - 1) * limit + }&verified_publisher=${verified}&cncf=${cncf}` ).then(response => response.json()); } @@ -35,3 +59,106 @@ export function fetchChartValues(packageID: string, packageVersion: string) { }, }).then(response => response.text()); } + + + +// } + + +// maybe we dont need all this? maybe I can just use a useEffect for a layout that +// filters the parameters FIRST and then when the last of it is rendered we normal search the rest? + + +// The way to do this: +// - use a [loadNonMatch, setLoadNonMatch] = useState(false) +// - this will be used to follow the navigation, maybe if we use the math.floor num and say +// if we use the math.floor num and say that the last page of charts that match the filter parameters +// - after the charts that are all rendered by filter, we set the loadnonmatch to true and load the other charts +// - is there a way to filter out the ones that have already been seen while navigating? + +// - !!! Maybe I can create some middleware that will note the current rendered charts and put them in the seen list +// and while navigating the tabs we add more charts to the seen list, and when we hit the end of the tabs +// that were available for that filter parameter (verified or "search" word) we then create a function +// that isplaced in between the return of the incoming response data and the function will filter +// the data that has been seen already and return the "new data", this way we can nav through +// the charts from "matches filter" to "non-matches filter" and back again without having to reload + +// - !!! we need a way to mathmatically track as to when we should be hitting the last of the charts +// that match the filter parameters, we can do this by using the total count of the charts that match +// - !!! when the number of charts that match has been reached i.e we have already rendered the charts +// then we can set the loadnonmatch to true and then we can start loading the other charts + +// - !!! we will load the other charts my making a generic search request with the same parameters +// only this time it will kick off the if (loadnonmatch) and then we will be able to navigate the normals +// within that if block the function of filterSeenCharts will be called and the data will be filtered + + + +export async function fetchAllItemCount(verified: boolean) { + + const response = await fetch(`https://artifacthub.io/api/v1/packages/search?offset=0&limit=4&facets=true&kind=0&deprecated=false&sort=relevance&verified_publisher=${verified}`) + const data = await response.json() + + console.log("data for item", data) + + // find the number within response.facets.options.[where the id field is 0].total + const total = data.facets.find((facet) => facet.filter_key === "kind").options.find((option) => option.id === 0).total + console.log("item total for helm", total) + + return data + +} + +export async function fetchChart(limit, offset, verified, cncf, search) { + const response = await fetch(`https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&sort=relevance&facets=true&limit=${limit}&offset=${offset}&verified_publisher=${verified}&cncf=${cncf}`) + const data = await response.json() + console.log("data", data.packages) + return data +} + +async function fetchAllCharts(total, verified, cncf, search) { + let allCharts = [] + let offset = 0 + + // we need to take the total and divide it by 60 to get the number of times we need to make the request + // we can then use the offset to keep track of the number of requests we have made + const totalRequests = Math.ceil(total / 60) + + + for (let i = 0; i < totalRequests; i++) { + const limit = Math.min(60, total - offset) + const data = await fetchChart(limit, offset, verified, cncf, search) + console.log("data", data.packages) + + if (data.packages.length < 9) { + console.log("LAST ITEMS OF CHARTS") + } + + if (data.packages.length > 0) { + allCharts = allCharts.concat(data.packages) + offset += limit + } + + console.log(`current length of allCharts ${allCharts.length} and offset ${offset} and limit ${limit} and totalRequests ${totalRequests} and total ${total}`) + } + + + console.log("length of allCharts", allCharts.length) + return allCharts +} + +export async function returnAllCharts(totalCharts, + official: boolean, + verified: boolean, + cncf: boolean, + search: string) { + try { + const charts = await fetchAllCharts(totalCharts, verified, cncf, search) + console.log("length of charts", charts.length) + // console log the last chart to see if we have all the charts + console.log("last chart", charts[charts.length - 1]) + } catch (error) { + console.log("error", error) + } +} + diff --git a/app-catalog/src/components/charts/List.tsx b/app-catalog/src/components/charts/List.tsx index c02eb1a..a6fd23c 100644 --- a/app-catalog/src/components/charts/List.tsx +++ b/app-catalog/src/components/charts/List.tsx @@ -17,11 +17,10 @@ import { Tooltip, Typography, } from '@mui/material'; -import { Autocomplete, Pagination } from '@mui/material'; +import { Autocomplete, ButtonGroup, Pagination } from '@mui/material'; import { useEffect, useState } from 'react'; //import { jsonToYAML, yamlToJSON } from '../../helpers'; -import { fetchChartsFromArtifact } from '../../api/charts'; -import CNCFLight from './cncf-icon-color.svg'; +import { fetchAllItemCount, fetchChartsFromArtifact } from '../../api/charts'; //import { createRelease } from '../../api/releases'; import { EditorDialog } from './EditorDialog'; @@ -45,46 +44,92 @@ export function ChartsList({ fetchCharts = fetchChartsFromArtifact }) { const [chartsTotalCount, setChartsTotalCount] = useState(0); const [chartCategory, setChartCategory] = useState(helmChartCategoryList[0]); const [search, setSearch] = useState(''); + const [chartOfficial, setChartOfficial] = useState(); + const [showVerified, setShowVerified] = useState(() => JSON.parse(localStorage.getItem('showVerified') ?? 'true')); + const [isCNCF, setIsCNCF] = useState(false); + const [loadNonMatching, setLoadNonMatching] = useState(false); + const [seenCharts, setSeenCharts] = useState([]); const [selectedChartForInstall, setSelectedChartForInstall] = useState(null); + + function filterSeenCharts(responseData) { + responseData.forEach( + (chart) => { + if (!seenCharts.includes(chart)) { + seenCharts.push(chart) + } + } + ) + + // later add an if statement here that if the math number of seenCharts.length matches the total of num chart total + // we set the loadNonMatching to true + if (Math.floor(chartsTotalCount / PAGE_OFFSET_COUNT_FOR_CHARTS) === page) { + setLoadNonMatching(true) + // returnAllCharts(chartsTotalCount, chartOfficial, chartVerified, isCNCF, search) + + } + + return responseData + } + + useEffect(() => { + setCharts(null); - fetchCharts(search, chartCategory, page).then(response => { - setCharts(response.packages); - const facets = response.facets; - const categoryOptions = facets.find( - (facet: { - title: string; - options: { - name: string; - total: number; - }[]; - }) => facet.title === 'Category' - ).options; - if (chartCategory.title === 'All') { - const totalCount = categoryOptions.reduce( - ( - acc: number, - option: { + + fetchCharts(search, showVerified, isCNCF, loadNonMatching, chartCategory, page).then( + response => { + setCharts(filterSeenCharts(response.packages)); + const facets = response.facets; + const categoryOptions = facets.find( + (facet: { + title: string; + options: { name: string; total: number; - } - ) => acc + option.total, - 0 - ); + }[]; + }) => facet.title === 'Category' + ).options; + + // total number of charts + if (chartCategory.title === 'All') { + const totalCount = categoryOptions.reduce( + ( + acc: number, + option: { + name: string; + total: number; + } + ) => acc + option.total, + 0 + ); + setChartsTotalCount(totalCount); + return; + } + + // total number of charts + const totalCount = categoryOptions.find( + (option: { name: string; total: number }) => option.name === chartCategory?.title + ).total; setChartsTotalCount(totalCount); - return; } - const totalCount = categoryOptions.find( - (option: { name: string; total: number }) => option.name === chartCategory?.title - ).total; - setChartsTotalCount(totalCount); - }); - }, [page, chartCategory, search]); + ); + + // how to turn boolean into string + fetchAllItemCount(showVerified) + + }, [chartOfficial, showVerified, showVerified, isCNCF, page, chartCategory, search]); useEffect(() => { setPage(1); - }, [chartCategory, search]); + }, [chartOfficial, showVerified, showVerified, isCNCF, chartCategory, search]); + + + function toggleShowVerified() { + setShowVerified(!showVerified) + console.log("SWITCH current showVerified", showVerified) + localStorage.setItem('showVerified', `${!showVerified}`) + } function Search() { return ( @@ -106,6 +151,59 @@ export function ChartsList({ fetchCharts = fetchChartsFromArtifact }) { ); } + function ToggleOptions() { + return ( + <> + + current num of seen charts {chartsTotalCount} + + + current local showUnverified is {`${showVerified}`} + + + current num of seen charts {chartsTotalCount} + + current loadNonMatch is {`${loadNonMatching}`} + + {/* */} + + + + + + ); + } + function CategoryForCharts() { return ( setEditorOpen(open)} /> - , ]} /> + , , ]} + /> {!charts ? ( - {`No charts found for ${ - search ? `search term: ${search}` : `category: ${chartCategory.title}` - }`} + {`No charts found for ${search ? `search term: ${search}` : `category: ${chartCategory.title}` + }`} ) : ( diff --git a/app-catalog/src/index.tsx b/app-catalog/src/index.tsx index ca3c1e7..b20138a 100644 --- a/app-catalog/src/index.tsx +++ b/app-catalog/src/index.tsx @@ -1,8 +1,9 @@ -import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; +import { registerPluginSettings, registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import ChartDetails from './components/charts/Details'; import { ChartsList } from './components/charts/List'; import ReleaseDetail from './components/releases/Detail'; import ReleaseList from './components/releases/List'; +import { AppCatalogSettings } from './settings'; export function isElectron(): boolean { // Renderer process @@ -92,3 +93,5 @@ if (isElectron()) { component: () => , }); } + +registerPluginSettings('app-catalog', AppCatalogSettings, true); diff --git a/app-catalog/src/settings.tsx b/app-catalog/src/settings.tsx new file mode 100644 index 0000000..35c4405 --- /dev/null +++ b/app-catalog/src/settings.tsx @@ -0,0 +1,135 @@ +import { + Box, + Paper, + Switch, + SwitchProps, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme +} from '@mui/material'; +import { useEffect, useState } from 'react'; + + +export function AppCatalogSettings() { + + const [showVerified, setShowVerified] = useState(() => JSON.parse(localStorage.getItem('showVerified') ?? 'true')); + + + // const { data, onDataChange } = props; + + // function onChange(value: string) { + // if (onDataChange) { + // onDataChange({ works: value }); + // } + // } + + useEffect(() => { + + }, []); + + function toggleShowVerified() { + setShowVerified(!showVerified) + console.log("SWITCH current showVerified", showVerified) + localStorage.setItem('showVerified', `${!showVerified}`) + } + + function ShowVerifiedSwitch() { + return ( + + + Show verified + + + + + + ) + } + + return ( + + + + + + Options + Enable + + + + + +
+
+ +
+ ); +} + +const EnableSwitch = (props: SwitchProps) => { + const theme = useTheme(); + + return ( + + ); +}; + +