From 90a46bd2465c0d600a7db62e159f9a2d0da3877d Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 6 Sep 2021 18:25:50 +0000 Subject: [PATCH 1/2] CollectionsFilter: use /_ui/v1/tags/{collections,roles} API to populate filter tags No-Issue --- src/api/base.ts | 2 +- src/api/index.ts | 1 + src/api/tag.ts | 15 +++++ .../collection-list/collection-filter.tsx | 63 ++++++++++++------- src/constants.tsx | 16 ----- .../ansible-role/namespace-detail.tsx | 19 +++++- src/containers/ansible-role/role-list.tsx | 20 +++++- 7 files changed, 91 insertions(+), 45 deletions(-) create mode 100644 src/api/tag.ts diff --git a/src/api/base.ts b/src/api/base.ts index 04363cbbda..a6857d72d9 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -67,7 +67,7 @@ export class BaseAPI { return this.http.patch(this.getPath(apiPath) + id + '/', data); } - getPath(apiPath: string) { + getPath(apiPath?: string) { return apiPath || this.apiPath; } diff --git a/src/api/index.ts b/src/api/index.ts index d35f5b28cc..753c56372e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -85,6 +85,7 @@ export { SettingsAPI } from './settings'; export { SignCollectionAPI } from './sign-collections'; export { SignContainersAPI } from './sign-containers'; export { SigningServiceAPI, SigningServiceType } from './signing-service'; +export { TagAPI } from './tag'; export { TaskAPI } from './task'; export { TaskManagementAPI } from './task-management'; export { UserAPI } from './user'; diff --git a/src/api/tag.ts b/src/api/tag.ts new file mode 100644 index 0000000000..6323bfc2e7 --- /dev/null +++ b/src/api/tag.ts @@ -0,0 +1,15 @@ +import { HubAPI } from './hub'; + +export class API extends HubAPI { + apiPath = this.getUIPath('tags/'); + + listCollections(params) { + return this.list(params, this.getPath() + 'collections/'); + } + + listRoles(params) { + return this.list(params, this.getPath() + 'roles/'); + } +} + +export const TagAPI = new API(); diff --git a/src/components/collection-list/collection-filter.tsx b/src/components/collection-list/collection-filter.tsx index 6cda4ace62..7136011ea5 100644 --- a/src/components/collection-list/collection-filter.tsx +++ b/src/components/collection-list/collection-filter.tsx @@ -7,9 +7,8 @@ import { } from '@patternfly/react-core'; import React from 'react'; import { useEffect, useState } from 'react'; -import { AnsibleRepositoryAPI } from 'src/api'; +import { AnsibleRepositoryAPI, TagAPI } from 'src/api'; import { AppliedFilters, CompoundFilter } from 'src/components'; -import { Constants } from 'src/constants'; import { useContext } from 'src/loaders/app-context'; import './collection-filter.scss'; @@ -28,27 +27,51 @@ interface IProps { export const CollectionFilter = (props: IProps) => { const context = useContext(); + const { ignoredParams, params, updateParams } = props; + const { display_signatures, display_repositories } = context.featureFlags; + const displayTags = ignoredParams.includes('tags') === false; + const displayRepos = + ignoredParams.includes('repository_name') === false && display_repositories; + const displayNamespaces = ignoredParams.includes('namespace') === false; + const [repositories, setRepositories] = useState([]); const [inputText, setInputText] = useState(''); const [selectedFilter, setSelectedFilter] = useState(null); + const [tags, setTags] = useState([]); const loadRepos = () => { AnsibleRepositoryAPI.list({ name__icontains: inputText, pulp_label_select: '!hide_from_search', - }).then((res) => { - const repos = res.data.results.map(({ name }) => ({ - id: name, - title: name, - })); - setRepositories(repos); - }); + }).then(({ data: { results } }) => + setRepositories( + results.map(({ name }) => ({ + id: name, + title: name, + })), + ), + ); + }; + + const loadTags = () => { + TagAPI.listCollections({ name__icontains: inputText, sort: '-count' }).then( + ({ data: { data } }) => + setTags( + data.map(({ name, count }) => ({ + id: name, + title: count === undefined ? name : t`${name} (${count})`, + })), + ), + ); }; useEffect(() => { if (selectedFilter === 'repository_name') { loadRepos(); } + if (selectedFilter === 'tags' && displayTags) { + loadTags(); + } }, [selectedFilter]); useEffect(() => { @@ -65,12 +88,11 @@ export const CollectionFilter = (props: IProps) => { } }, [inputText]); - const { ignoredParams, params, updateParams } = props; - const { display_signatures, display_repositories } = context.featureFlags; - const displayTags = ignoredParams.includes('tags') === false; - const displayRepos = - ignoredParams.includes('repository_name') === false && display_repositories; - const displayNamespaces = ignoredParams.includes('namespace') === false; + useEffect(() => { + if (inputText != '' && selectedFilter === 'tags' && displayTags) { + loadTags(); + } + }, [inputText]); const filterConfig = [ { @@ -90,11 +112,8 @@ export const CollectionFilter = (props: IProps) => { displayTags && { id: 'tags', title: t`Tag`, - inputType: 'multiple' as const, - options: Constants.COLLECTION_FILTER_TAGS.map((tag) => ({ - id: tag, - title: tag, - })), + inputType: 'typeahead' as const, + options: tags, }, display_signatures && { id: 'is_signed', @@ -118,9 +137,7 @@ export const CollectionFilter = (props: IProps) => { updateParams={updateParams} params={params} filterConfig={filterConfig} - selectFilter={(selected) => { - setSelectedFilter(selected); - }} + selectFilter={setSelectedFilter} /> + data.map(({ name, count }) => ({ + id: name, + title: count === undefined ? name : t`${name} (${count})`, + })), + ) + .catch(() => []); + } + private get updateParams() { return ParamHelper.updateParamsMixin(); } render() { + const { count, loading, params, roles } = this.state; + const updateParams = (params) => this.updateParams(params, () => this.query(params)); @@ -135,6 +149,8 @@ class NamespaceRoles extends React.Component< { id: 'tags', title: t`Tags`, + inputType: 'typeahead' as const, + // options handled by `typeaheads` }, ]; @@ -152,8 +168,6 @@ class NamespaceRoles extends React.Component< }, ]; - const { count, loading, params, roles } = this.state; - const noData = count === 0 && !filterIsSet( @@ -178,6 +192,7 @@ class NamespaceRoles extends React.Component< ignoredParams={['page', 'page_size', 'sort']} params={params} sortOptions={sortOptions} + typeaheads={{ tags: this.loadTags }} updateParams={updateParams} /> {!count ? ( diff --git a/src/containers/ansible-role/role-list.tsx b/src/containers/ansible-role/role-list.tsx index c968774994..b073e9df76 100644 --- a/src/containers/ansible-role/role-list.tsx +++ b/src/containers/ansible-role/role-list.tsx @@ -1,7 +1,7 @@ import { t } from '@lingui/macro'; import { DataList } from '@patternfly/react-core'; import React from 'react'; -import { LegacyRoleAPI, LegacyRoleListType } from 'src/api'; +import { LegacyRoleAPI, LegacyRoleListType, TagAPI } from 'src/api'; import { AlertList, AlertType, @@ -80,6 +80,17 @@ class AnsibleRoleList extends React.Component { ); } + loadTags(inputText) { + return TagAPI.listRoles({ name__icontains: inputText, sort: '-count' }) + .then(({ data: { data } }) => + data.map(({ name, count }) => ({ + id: name, + title: count === undefined ? name : t`${name} (${count})`, + })), + ) + .catch(() => []); + } + private get updateParams() { return ParamHelper.updateParamsMixin(); } @@ -95,6 +106,8 @@ class AnsibleRoleList extends React.Component { } render() { + const { alerts, count, loading, params, roles } = this.state; + const updateParams = (params) => this.updateParams(params, () => this.query(params)); @@ -110,6 +123,8 @@ class AnsibleRoleList extends React.Component { { id: 'tags', title: t`Tags`, + inputType: 'typeahead' as const, + // options handled by `typeaheads` }, ]; @@ -127,8 +142,6 @@ class AnsibleRoleList extends React.Component { }, ]; - const { alerts, count, loading, params, roles } = this.state; - const noData = count === 0 && !filterIsSet( @@ -155,6 +168,7 @@ class AnsibleRoleList extends React.Component { ignoredParams={['page', 'page_size', 'sort']} params={params} sortOptions={sortOptions} + typeaheads={{ tags: this.loadTags }} updateParams={updateParams} /> From fb547b1d1286d50dd2980c78057904cc4f34de10 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Wed, 1 Nov 2023 03:31:18 +0000 Subject: [PATCH 2/2] HubListToolbar - support typeaheads --- src/components/shared/hub-list-toolbar.tsx | 48 +++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/components/shared/hub-list-toolbar.tsx b/src/components/shared/hub-list-toolbar.tsx index 1071ef7d26..aa4b330974 100644 --- a/src/components/shared/hub-list-toolbar.tsx +++ b/src/components/shared/hub-list-toolbar.tsx @@ -4,7 +4,7 @@ import { ToolbarGroup, ToolbarItem, } from '@patternfly/react-core'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AppliedFilters, CompoundFilter, @@ -21,24 +21,61 @@ interface IProps { ignoredParams: string[]; params: ParamType; sortOptions?: SortFieldType[]; + typeaheads?: Record< + string, + (inputText: string) => Promise<{ id: string; title: string }[]> + >; updateParams: (p) => void; } +function useTypeaheads(typeaheads, { inputText, selectedFilter }) { + const [options, setOptions] = useState({}); + const loader = typeaheads[selectedFilter]; + const setter = (value) => + setOptions((options) => ({ ...options, [selectedFilter]: value })); + + useEffect(() => { + if (selectedFilter && loader) { + loader('').then(setter); + } + }, [selectedFilter]); + + useEffect(() => { + if (inputText && loader) { + loader(inputText).then(setter); + } + }, [inputText]); + + return options; +} + // FIXME: missing Buttons & CardListSwitcher to be usable everywhere export function HubListToolbar({ + count, + filterConfig, ignoredParams, params, - updateParams, - filterConfig, sortOptions, - count, + typeaheads, + updateParams, }: IProps) { const [inputText, setInputText] = useState(''); + const [selectedFilter, setSelectedFilter] = useState(null); + const typeaheadOptions = useTypeaheads(typeaheads || {}, { + inputText, + selectedFilter, + }); const niceNames = Object.fromEntries( filterConfig.map(({ id, title }) => [id, title]), ); + const filterWithOptions = filterConfig.map((item) => + item.inputType !== 'typeahead' + ? item + : { ...item, options: item.options || typeaheadOptions[item.id] || [] }, + ); + return ( @@ -51,10 +88,11 @@ export function HubListToolbar({ >