From 5c2a6133b1aadc5709e522133942b6ff6928e049 Mon Sep 17 00:00:00 2001 From: pratap0007 Date: Wed, 17 Mar 2021 11:26:01 +0530 Subject: [PATCH] Adds selected filters in the url as a query parameter This patch updates url based on the selected filters, search query and sort by Signed-off-by: Shiv Verma --- ui/src/common/params.ts | 7 +++ .../App/__snapshots__/App.test.tsx.snap | 9 ++-- ui/src/containers/App/index.tsx | 2 + ui/src/containers/ParseUrl/ParseUrl.test.tsx | 24 +++++++++ ui/src/containers/ParseUrl/index.tsx | 23 +++++++++ .../__snapshots__/Resources.test.tsx.snap | 4 +- ui/src/containers/Resources/index.tsx | 32 ++++++++---- ui/src/containers/Search/index.tsx | 27 +--------- ui/src/store/catalog.test.ts | 21 ++++++++ ui/src/store/catalog.ts | 13 +++++ ui/src/store/category.test.ts | 50 +++++++++++++++++++ ui/src/store/category.ts | 14 ++++++ ui/src/store/resource.test.ts | 23 +++++++++ ui/src/store/resource.ts | 32 ++++++++++++ ui/src/utils/updateUrl.test.ts | 8 +++ ui/src/utils/updateUrl.ts | 33 ++++++++++++ 16 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 ui/src/common/params.ts create mode 100644 ui/src/containers/ParseUrl/ParseUrl.test.tsx create mode 100644 ui/src/containers/ParseUrl/index.tsx create mode 100644 ui/src/utils/updateUrl.test.ts create mode 100644 ui/src/utils/updateUrl.ts diff --git a/ui/src/common/params.ts b/ui/src/common/params.ts new file mode 100644 index 0000000000..260a326b58 --- /dev/null +++ b/ui/src/common/params.ts @@ -0,0 +1,7 @@ +export enum Params { + Query = 'query', + SortBY = 'sortBy', + Category = 'category', + Kind = 'kind', + Catalog = 'catalog' +} diff --git a/ui/src/containers/App/__snapshots__/App.test.tsx.snap b/ui/src/containers/App/__snapshots__/App.test.tsx.snap index 457d8790f3..0a3b9f9a0a 100644 --- a/ui/src/containers/App/__snapshots__/App.test.tsx.snap +++ b/ui/src/containers/App/__snapshots__/App.test.tsx.snap @@ -8,6 +8,9 @@ exports[`App should render the component correctly and match the snapshot 1`] = + + +
@@ -245,8 +248,8 @@ exports[`App should render the component correctly and match the snapshot 1`] =
- - + + @@ -254,7 +257,7 @@ exports[`App should render the component correctly and match the snapshot 1`] = - +
diff --git a/ui/src/containers/App/index.tsx b/ui/src/containers/App/index.tsx index 4a4b637789..62d7626eaa 100644 --- a/ui/src/containers/App/index.tsx +++ b/ui/src/containers/App/index.tsx @@ -10,6 +10,7 @@ import Footer from '../../components/Footer'; import Resources from '../Resources'; import Authentication from '../../containers/Authentication'; import Details from '../Details'; +import ParseUrl from '../ParseUrl'; import { createProvider } from '../../store/root'; import './App.css'; @@ -19,6 +20,7 @@ const App: React.FC = observer(() => { return ( + } className="hub-page"> diff --git a/ui/src/containers/ParseUrl/ParseUrl.test.tsx b/ui/src/containers/ParseUrl/ParseUrl.test.tsx new file mode 100644 index 0000000000..e7d3017cf0 --- /dev/null +++ b/ui/src/containers/ParseUrl/ParseUrl.test.tsx @@ -0,0 +1,24 @@ +import { when } from 'mobx'; +import { FakeHub } from '../../api/testutil'; +import { createProviderAndStore } from '../../store/root'; + +const TESTDATA_DIR = `src/store/testdata`; +const api = new FakeHub(TESTDATA_DIR); +const { root } = createProviderAndStore(api); + +describe('ParseUrl component', () => { + it('it can set url params to resource store', (done) => { + const { resources } = root; + when( + () => { + return !resources.isLoading; + }, + () => { + resources.setURLParams('?/query=ansible'); + expect(resources.urlParams).toBe('?/query=ansible'); + + done(); + } + ); + }); +}); diff --git a/ui/src/containers/ParseUrl/index.tsx b/ui/src/containers/ParseUrl/index.tsx new file mode 100644 index 0000000000..9977d835ed --- /dev/null +++ b/ui/src/containers/ParseUrl/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useMst } from '../../store/root'; +import { Params } from '../../common/params'; + +const ParseUrl: React.FC = () => { + const { resources } = useMst(); + + if (window.location.search) { + const searchParams = new URLSearchParams(window.location.search); + if (searchParams.has(Params.Query)) { + resources.setSearch(searchParams.get(Params.Query)); + } + if (searchParams.has(Params.SortBY)) { + resources.setSortBy(searchParams.get(Params.SortBY)); + } + + // Storing url params to store inorder to parse the url only after successfully resource load + resources.setURLParams(window.location.search); + } + + return <> ; +}; +export default ParseUrl; diff --git a/ui/src/containers/Resources/__snapshots__/Resources.test.tsx.snap b/ui/src/containers/Resources/__snapshots__/Resources.test.tsx.snap index e89791eff8..f32981f038 100644 --- a/ui/src/containers/Resources/__snapshots__/Resources.test.tsx.snap +++ b/ui/src/containers/Resources/__snapshots__/Resources.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Resource Component should render the resources component 1`] = ` - +
@@ -740,7 +740,7 @@ exports[`Resource Component should render the resources component 1`] = `
-
+
diff --git a/ui/src/containers/Resources/index.tsx b/ui/src/containers/Resources/index.tsx index b824bc0b21..62391f0bd2 100644 --- a/ui/src/containers/Resources/index.tsx +++ b/ui/src/containers/Resources/index.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { useObserver } from 'mobx-react'; +import React, { useEffect } from 'react'; +import { observer } from 'mobx-react'; import { EmptyState, EmptyStateIcon, @@ -14,12 +14,24 @@ import { useHistory } from 'react-router-dom'; import { useMst } from '../../store/root'; import { IResource } from '../../store/resource'; import Cards from '../../components/Cards'; +import { UpdateURL } from '../../utils/updateUrl'; import './Resources.css'; -const Resources = () => { - const { resources } = useMst(); +const Resources: React.FC = observer(() => { + const { resources, categories } = useMst(); + const { catalogs, kinds, search, sortBy } = resources; const history = useHistory(); + + useEffect(() => { + const selectedcategories = categories.selected.join(','); + const selectedKinds = [...kinds.selected].join(','); + const selectedCatalogs = catalogs.selectedByName.join(','); + + const url = UpdateURL(search, sortBy, selectedcategories, selectedKinds, selectedCatalogs); + if (!resources.isLoading) history.replace(`?${url}`); + }, [search, sortBy, categories.selected, kinds.selected, catalogs.selected]); + const clearFilter = () => { resources.clearAllFilters(); history.push('/'); @@ -43,12 +55,10 @@ const Resources = () => { ); }; - return useObserver(() => - resources.resources.size === 0 ? ( - - ) : ( - {checkResources(resources.filteredResources)} - ) + return resources.resources.size === 0 ? ( + + ) : ( + {checkResources(resources.filteredResources)} ); -}; +}); export default Resources; diff --git a/ui/src/containers/Search/index.tsx b/ui/src/containers/Search/index.tsx index f31fbc3e18..2c35bd1ad8 100644 --- a/ui/src/containers/Search/index.tsx +++ b/ui/src/containers/Search/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { useObserver } from 'mobx-react'; import { TextInput } from '@patternfly/react-core'; @@ -9,35 +9,13 @@ import './Search.css'; const Search: React.FC = () => { const { resources } = useMst(); - // to get query params from the url - const searchParams = new URLSearchParams(window.location.search); - const query = searchParams.get('query') || ' '; - - useEffect(() => { - if (query !== ' ') { - resources.setSearch(query); - } - }, [query, resources]); - - const setParams = ({ query = '' }) => { - const searchParams = new URLSearchParams(); - searchParams.set('query', query); - return searchParams.toString(); - }; - - const updateURL = (text: string) => { - const url = setParams({ query: text }); - if (window.location.pathname === '/') history.replace(`?${url}`); - }; - const onSearchChange = useDebounce(resources.search, 400); const history = useHistory(); const onSearchKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); - history.push('/'); - updateURL(resources.search); + if (window.location.pathname !== '/') history.push('/'); } return; }; @@ -48,7 +26,6 @@ const Search: React.FC = () => { type="search" onChange={(resourceName: string) => { resources.setSearch(resourceName); - updateURL(resourceName); return onSearchChange; }} onKeyPress={onSearchKeyPress} diff --git a/ui/src/store/catalog.test.ts b/ui/src/store/catalog.test.ts index 0caf02a1df..c558b537c6 100644 --- a/ui/src/store/catalog.test.ts +++ b/ui/src/store/catalog.test.ts @@ -90,4 +90,25 @@ describe('Store Object', () => { done(); }); + + it('should toggle catalogs by name and can get selected catlogs by name', (done) => { + const store = CatalogStore.create({}); + + const item = Catalog.create({ + id: 1, + name: 'tekton', + type: 'community' + }); + + store.add(item); + + store.toggleByName('tekton'); + const catalogs = store.items.get('1'); + assert(catalogs); + + expect(catalogs.selected).toBe(true); + expect(store.selectedByName).toEqual(['tekton']); + + done(); + }); }); diff --git a/ui/src/store/catalog.ts b/ui/src/store/catalog.ts index d1fdf9ebc6..2ebf72fd26 100644 --- a/ui/src/store/catalog.ts +++ b/ui/src/store/catalog.ts @@ -41,6 +41,13 @@ export const CatalogStore = types self.items.forEach((c) => { c.selected = false; }); + }, + toggleByName(name: string) { + self.items.forEach((c) => { + if (c.name === name) { + c.selected = true; + } + }); } })) @@ -58,5 +65,11 @@ export const CatalogStore = types }); return list; + }, + + get selectedByName() { + return Array.from(self.items.values()) + .filter((c: ICatalog) => c.selected) + .reduce((acc: string[], c: ICatalog) => [...acc, c.name], []); } })); diff --git a/ui/src/store/category.test.ts b/ui/src/store/category.test.ts index 5d7bb52447..c77df745d6 100644 --- a/ui/src/store/category.test.ts +++ b/ui/src/store/category.test.ts @@ -122,4 +122,54 @@ describe('Store functions', () => { } ); }); + + it('can toggle the category by name', (done) => { + const store = CategoryStore.create({}, { api }); + expect(store.count).toBe(0); + expect(store.isLoading).toBe(true); + + when( + () => !store.isLoading, + () => { + expect(store.count).toBe(5); + expect(store.isLoading).toBe(false); + + store.toggleByName('Build Tools'); + + const categories = store.items.get('1'); + assert(categories); + expect(categories.selected).toBe(true); + + done(); + } + ); + }); + + it('can return the all selected catgories in a list', (done) => { + const store = CategoryStore.create({}, { api }); + expect(store.count).toBe(0); + expect(store.isLoading).toBe(true); + + when( + () => !store.isLoading, + () => { + expect(store.count).toBe(5); + expect(store.isLoading).toBe(false); + + // Gets the category with id as 1 + const c1 = store.items.get('1'); + assert(c1); + c1.toggle(); + + // Gets the category with id as 2 + const c2 = store.items.get('2'); + assert(c2); + c2.toggle(); + + expect(store.selected.sort()).toEqual(['Build Tools', 'CLI'].sort()); + + done(); + } + ); + }); }); diff --git a/ui/src/store/category.ts b/ui/src/store/category.ts index ce8f2b1c25..3af5e381ca 100644 --- a/ui/src/store/category.ts +++ b/ui/src/store/category.ts @@ -45,6 +45,12 @@ export const CategoryStore = types return Array.from(self.items.values()); }, + get selected() { + return Array.from(self.items.values()) + .filter((c: ICategory) => c.selected) + .reduce((acc: string[], c: ICategory) => [...acc, c.name], []); + }, + get selectedTags() { return new Set( Array.from(self.items.values()) @@ -67,6 +73,14 @@ export const CategoryStore = types self.items.forEach((c) => { c.selected = false; }); + }, + + toggleByName(name: string) { + self.items.forEach((c) => { + if (c.name === name) { + c.selected = true; + } + }); } })) diff --git a/ui/src/store/resource.test.ts b/ui/src/store/resource.test.ts index 09bd7926d4..c415f9452a 100644 --- a/ui/src/store/resource.test.ts +++ b/ui/src/store/resource.test.ts @@ -648,4 +648,27 @@ describe('Store functions', () => { } ); }); + + it('it should parse the url and can update the store', (done) => { + const store = ResourceStore.create( + {}, + { + api, + categories: CategoryStore.create({}, { api }) + } + ); + expect(store.isLoading).toBe(true); + when( + () => !store.isLoading, + () => { + store.setURLParams('?category=Automation%2CBuild+Tools&catalog=tekton'); + store.parseUrl(); + + expect(store.filteredResources.length).toBe(1); + expect(store.categories.selected).toEqual(['Build Tools']); + + done(); + } + ); + }); }); diff --git a/ui/src/store/resource.ts b/ui/src/store/resource.ts index a1767a0c20..5c1e8ff484 100644 --- a/ui/src/store/resource.ts +++ b/ui/src/store/resource.ts @@ -7,6 +7,7 @@ import { Api } from '../api'; import { Catalog, CatalogStore } from './catalog'; import { Kind, KindStore } from './kind'; import { assert } from './utils'; +import { Params } from '../common/params'; export const updatedAt = types.custom({ name: 'momentDate', @@ -98,6 +99,7 @@ export const ResourceStore = types sortBy: types.optional(types.enumeration(Object.values(SortByFields)), SortByFields.Unknown), tags: types.optional(types.map(Tag), {}), search: '', + urlParams: '', err: '', isLoading: true }) @@ -126,6 +128,9 @@ export const ResourceStore = types setSortBy(field: string) { const key: SortByFields = SortByFields[field as keyof typeof SortByFields]; self.sortBy = key; + }, + setURLParams(url: string) { + self.urlParams = url; } })) @@ -136,6 +141,30 @@ export const ResourceStore = types self.categories.clearSelected(); self.setSearch(''); self.setSortBy(SortByFields.Unknown); + }, + parseUrl() { + const searchParams = new URLSearchParams(self.urlParams); + if (searchParams.has(Params.Category)) { + const categoriesParams = searchParams.getAll(Params.Category)[0].split(','); + categoriesParams.forEach((t: string) => { + self.categories.toggleByName(t); + }); + } + + if (searchParams.has(Params.Catalog)) { + const catalogsParams = searchParams.getAll(Params.Catalog)[0].split(','); + catalogsParams.forEach((t: string) => { + self.catalogs.toggleByName(t); + }); + } + if (searchParams.has(Params.Kind)) { + const kindsParams = searchParams.getAll(Params.Kind)[0].split(','); + kindsParams.forEach((t: string) => { + const kind = self.kinds.items.get(t); + assert(kind); + kind.toggle(); + }); + } } })) @@ -238,6 +267,9 @@ export const ResourceStore = types r.versions.push(r.latestVersion); self.add(r); }); + + // url parsing after resource load + if (self.urlParams) self.parseUrl(); } catch (err) { self.err = err.toString(); } diff --git a/ui/src/utils/updateUrl.test.ts b/ui/src/utils/updateUrl.test.ts new file mode 100644 index 0000000000..acf8ea133f --- /dev/null +++ b/ui/src/utils/updateUrl.test.ts @@ -0,0 +1,8 @@ +import { UpdateURL } from './updateUrl'; + +describe('Test UpdateUrl function', () => { + it('Test UpdateUrl function', () => { + const val = UpdateURL('ansible', 'rating', 'cli', 'task', 'tekton'); + expect(val).toEqual('query=ansible&sortBy=rating&category=cli&kind=task&catalog=tekton'); + }); +}); diff --git a/ui/src/utils/updateUrl.ts b/ui/src/utils/updateUrl.ts new file mode 100644 index 0000000000..7c464ead62 --- /dev/null +++ b/ui/src/utils/updateUrl.ts @@ -0,0 +1,33 @@ +import { Params } from '../common/params'; +import { SortByFields } from '../store/resource'; + +export const UpdateURL = ( + search: string, + sort: string, + categories: string, + kinds: string, + catalogs: string +) => { + const searchParams = new URLSearchParams(window.location.search); + if (!search && searchParams.has(Params.Query)) searchParams.delete(Params.Query); + + if (search) searchParams.set(Params.Query, search); + + if (sort === SortByFields.Unknown && searchParams.has(Params.SortBY)) + searchParams.delete(Params.SortBY); + if (sort !== SortByFields.Unknown) searchParams.set(Params.SortBY, sort); + + if (!categories && searchParams.has(Params.Category)) searchParams.delete(Params.Category); + + if (categories) searchParams.set(Params.Category, categories); + + if (!kinds && searchParams.has(Params.Kind)) searchParams.delete(Params.Kind); + + if (kinds) searchParams.set(Params.Kind, kinds); + + if (!catalogs && searchParams.has(Params.Catalog)) searchParams.delete(Params.Catalog); + + if (catalogs) searchParams.set(Params.Catalog, catalogs); + + return searchParams.toString(); +};