From 770b7206c9851beb0cf6ba738877112cf0e06172 Mon Sep 17 00:00:00 2001 From: lfjnascimento Date: Tue, 23 Jul 2024 12:09:12 -0300 Subject: [PATCH 1/6] fix: minor adjust in api/TreeDetails --- dashboard/src/api/TreeDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/api/TreeDetails.tsx b/dashboard/src/api/TreeDetails.tsx index 5168a388..3a5af9f7 100644 --- a/dashboard/src/api/TreeDetails.tsx +++ b/dashboard/src/api/TreeDetails.tsx @@ -12,7 +12,7 @@ const fetchTreeDetailData = async ( Object.keys(filter).forEach(key => { const filterList = filter[key as keyof TTreeDetailsFilter]; - filterList?.map(value => + filterList?.forEach(value => filterParam.append(`filter_${key}`, value.toString()), ); }); From f2530d49ebcecd914bbe88f7e4a9529448406c72 Mon Sep 17 00:00:00 2001 From: lfjnascimento Date: Tue, 23 Jul 2024 12:10:13 -0300 Subject: [PATCH 2/6] refactor: adjustments in FilterList - make onClickItemHandler pass the value to onClickItem - fix typo itens - fix color on hover on cleanAll button --- .../FilterList/FilterList.stories.tsx | 4 +- .../src/components/FilterList/FilterList.tsx | 21 +++---- .../TreeDetails/TreeDetailsFilterList.tsx | 60 +++++++++++++++++++ 3 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 dashboard/src/routes/TreeDetails/TreeDetailsFilterList.tsx diff --git a/dashboard/src/components/FilterList/FilterList.stories.tsx b/dashboard/src/components/FilterList/FilterList.stories.tsx index cde629bc..95859bf6 100644 --- a/dashboard/src/components/FilterList/FilterList.stories.tsx +++ b/dashboard/src/components/FilterList/FilterList.stories.tsx @@ -31,7 +31,7 @@ type Story = StoryObj; export const Default: Story = { args: { - itens: ['linux-5.15.y', 'Status:failed', 'Status: Warnings'], + items: ['linux-5.15.y', 'Status:failed', 'Status: Warnings'], }, decorators: [ (story): JSX.Element => ( @@ -47,7 +47,7 @@ export const Default: Story = { export const MultipleLines: Story = { args: { - itens: [ + items: [ 'linux-5.15.y', 'Status:failed', 'Status: Warnings', diff --git a/dashboard/src/components/FilterList/FilterList.tsx b/dashboard/src/components/FilterList/FilterList.tsx index 43149007..632fa814 100644 --- a/dashboard/src/components/FilterList/FilterList.tsx +++ b/dashboard/src/components/FilterList/FilterList.tsx @@ -5,16 +5,16 @@ import { useIntl } from 'react-intl'; import { Button } from '../ui/button'; -interface IFilterList { - itens: string[]; - onClickItem: (itemIdx: number) => void; +export interface IFilterList { + items: string[]; + onClickItem: (item: string, itemIdx: number) => void; onClickCleanAll: () => void; removeOnEmpty?: boolean; } interface IFilterItem extends IFilterButton { idx: number; - onClickItem: (idx: number) => void; + onClickItem: (item: string, itemIdx: number) => void; } export interface IFilterButton @@ -57,8 +57,8 @@ const FilterItem = ({ ...props }: IFilterItem): JSX.Element => { const onClickHandler = useCallback( - () => onClickItem(idx), - [onClickItem, idx], + () => onClickItem(text, idx), + [text, onClickItem, idx], ); return ( @@ -72,7 +72,7 @@ const FilterItem = ({ }; const FilterList = ({ - itens, + items, onClickItem, onClickCleanAll, removeOnEmpty = false, @@ -81,7 +81,7 @@ const FilterList = ({ const buttonList = useMemo( () => - itens.map((item, idx) => ( + items.map((item, idx) => ( )), - [itens, onClickItem], + [items, onClickItem], ); - if (removeOnEmpty && !itens) { + if (removeOnEmpty && !items.length) { return <>; } @@ -100,6 +100,7 @@ const FilterList = ({
{buttonList} void; +} + +const createFlatFilter = (filter: TFilter): string[] => { + const flatFilter: string[] = []; + + Object.entries(filter).forEach(([field, values]) => { + Object.entries(values).forEach(([value, isSelected]) => { + if (isSelected) flatFilter.push(`${field}:${value}`); + }); + }); + return flatFilter; +}; + +const TreeDetailsFilterList = ({ + filter, + onFilter, +}: ITreeDetailsFilterList): JSX.Element => { + const flatFilter = useMemo(() => createFlatFilter(filter), [filter]); + + const onClickItem = useCallback( + (flatValue: string, _: number) => { + const [field, value] = flatValue.split(':'); + const newFilter = { ...filter }; + newFilter[field as TFilterKeys][value] = false; + onFilter(newFilter); + }, + [filter, onFilter], + ); + + const onClickCleanALl = useCallback(() => { + const newFilter = { ...filter }; + + flatFilter.forEach(flatValue => { + const [field, value] = flatValue.split(':'); + newFilter[field as TFilterKeys][value] = false; + }); + + onFilter(newFilter); + }, [filter, onFilter, flatFilter]); + + return ( + + ); +}; + +export default TreeDetailsFilterList; From 170b34a31fa5d427269bbca678331758b5f9d1e1 Mon Sep 17 00:00:00 2001 From: lfjnascimento Date: Tue, 23 Jul 2024 12:19:54 -0300 Subject: [PATCH 3/6] refactor: make the TreeDetails page own the filter - make the TreeDetails page own the filter instead of TreeDetailsFilter so we can pass the filter to other components like FilterList --- .../src/routes/TreeDetails/TreeDetails.tsx | 35 ++-- .../routes/TreeDetails/TreeDetailsFilter.tsx | 155 ++++++++++++------ 2 files changed, 120 insertions(+), 70 deletions(-) diff --git a/dashboard/src/routes/TreeDetails/TreeDetails.tsx b/dashboard/src/routes/TreeDetails/TreeDetails.tsx index c115711e..353fc584 100644 --- a/dashboard/src/routes/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/routes/TreeDetails/TreeDetails.tsx @@ -1,19 +1,19 @@ import { useParams } from 'react-router-dom'; -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { useTreeDetails } from '@/api/TreeDetails'; import TreeDetailsTab from '@/components/Tabs/TreeDetailsTab'; import { IListingItem } from '@/components/ListingItem/ListingItem'; import { ISummaryItem } from '@/components/Summary/Summary'; -import { - AccordionItemBuilds, - Results, - TTreeDetailsFilter, - TreeDetails as TreeDetailsType, -} from '@/types/tree/TreeDetails'; +import { AccordionItemBuilds, Results } from '@/types/tree/TreeDetails'; + +import TreeDetailsFilter, { + createFilter, + mapFilterToReq, + TFilter, +} from './TreeDetailsFilter'; -import TreeDetailsFilter from './TreeDetailsFilter'; export interface ITreeDetails { archs: ISummaryItem[]; @@ -24,15 +24,15 @@ export interface ITreeDetails { const TreeDetails = (): JSX.Element => { const { treeId } = useParams(); - const [filter, setFilter] = useState< - TTreeDetailsFilter | Record - >({}); - const { data } = useTreeDetails(treeId ?? '', filter); + const [filter, setFilter] = useState({}); + const reqFilter = mapFilterToReq(filter); + + const { data } = useTreeDetails(treeId ?? '', reqFilter); const [treeDetailsData, setTreeDetailsData] = useState(); - const initialDataRef = useRef(); - if (!initialDataRef.current) { - initialDataRef.current = data; + + if (data && Object.keys(filter).length === 0) { + setFilter(createFilter(data)); } useEffect(() => { @@ -94,10 +94,7 @@ const TreeDetails = (): JSX.Element => {
- +
diff --git a/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx index cdaa5ea0..2f1d8588 100644 --- a/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx +++ b/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import FilterDrawer from '@/components/Filter/Drawer'; @@ -6,99 +6,150 @@ import FilterSummarySection from '@/components/Filter/SummarySection'; import FilterCheckboxSection, { ICheckboxSection, } from '@/components/Filter/CheckboxSection'; -import { - TreeDetails as TreeDetailsType, - TTreeDetailsFilter, -} from '@/types/tree/TreeDetails'; +import { TreeDetails as TreeDetailsType } from '@/types/tree/TreeDetails'; + +type TFilterValues = { [key: string]: boolean }; +export type TFilter = + | { [key in TFilterKeys]: TFilterValues } + | Record; interface ITreeDetailsFilter { - data?: TreeDetailsType; - onFilter: (filter: TTreeDetailsFilter) => void; + filter: TFilter; + onFilter: (filter: TFilter) => void; } -type TFilterApplied = { [key: string]: boolean }; +export type TFilterKeys = (typeof filterFieldMap)[keyof typeof filterFieldMap]; + +const filterFieldMap = { + git_repository_branch: 'branches', + config_name: 'configs', + architecture: 'archs', + valid: 'status', +} as const; + +export const mapFilterToReq = ( + filter: TFilter, +): { [key: string]: string[] } => { + const filterMapped: { [key: string]: string[] } = {}; + + Object.entries(filterFieldMap).forEach(([reqField, field]) => { + const values = filter[field]; + if (!values) return; + + Object.entries(values).forEach(([value, isSelected]) => { + if (isSelected) { + if (reqField == 'valid') { + value = value == 'Valid' ? 'true' : 'false'; + } + if (!filterMapped[reqField]) filterMapped[reqField] = []; + filterMapped[reqField].push(value); + } + }); + }); -const sanitizeData = ( - data: TreeDetailsType | undefined, -): [TFilterApplied, TFilterApplied, TFilterApplied, TFilterApplied, string] => { - const status = { TRUE: false, FALSE: false }; - const branches: TFilterApplied = {}; - const configs: TFilterApplied = {}; - const archs: TFilterApplied = {}; - let treeUrl = ''; + return filterMapped; +}; + +export const createFilter = (data: TreeDetailsType | undefined): TFilter => { + const status = { Valid: false, Invalid: false }; + const branches: TFilterValues = {}; + const configs: TFilterValues = {}; + const archs: TFilterValues = {}; if (data) data.builds.forEach(b => { if (b.git_repository_branch) branches[b.git_repository_branch] = false; if (b.config_name) configs[b.config_name] = false; if (b.architecture) archs[b.architecture] = false; - if (!treeUrl && b.git_repository_url) treeUrl = b.git_repository_url; }); - return [status, branches, configs, archs, treeUrl]; + return { status, branches, configs, archs }; }; -const getFilterListFromObj = (filterObj: TFilterApplied): string[] => - Object.keys(filterObj).filter(key => filterObj[key]); +const changeFilterValue = ( + filter: TFilter, + filterField: TFilterKeys, + value: string, + isSelected: boolean, +): TFilter => { + const newFilter = { ...filter }; + if (newFilter[filterField]) { + newFilter[filterField][value] = isSelected; + } else { + newFilter[filterField] = { + [value]: isSelected, + }; + } + + return newFilter; +}; const TreeDetailsFilter = ({ - data, + filter, onFilter, }: ITreeDetailsFilter): JSX.Element => { const intl = useIntl(); - - const [statusObj, branchObj, configObj, archObj, treeUrl] = useMemo( - () => sanitizeData(data), - [data], - ); + const [diffFilter, setDiffFilter] = useState({}); const onClickFilterHandle = useCallback(() => { - const filter: TTreeDetailsFilter = {}; - - filter.config_name = getFilterListFromObj(configObj); - filter.git_repository_branch = getFilterListFromObj(branchObj); - filter.architecture = getFilterListFromObj(archObj); - filter.valid = getFilterListFromObj(statusObj); + const newFilter = { ...filter }; + Object.entries(diffFilter).forEach(([key, value]) => { + const typedKey = key as keyof typeof diffFilter; + + newFilter[typedKey] = { + ...newFilter[typedKey], + ...value, + }; + }); - onFilter(filter); - }, [onFilter, configObj, branchObj, archObj, statusObj]); + onFilter(newFilter); + setDiffFilter({}); + }, [filter, diffFilter, onFilter]); const checkboxSectionsProps: ICheckboxSection[] = useMemo(() => { return [ { title: intl.formatMessage({ id: 'global.branch' }), subtitle: intl.formatMessage({ id: 'filter.branchSubtitle' }), - items: branchObj, - onClickItem: (branch: string, isChecked: boolean) => - (branchObj[branch] = isChecked), + items: filter.branches, + onClickItem: (value: string, isChecked: boolean): void => { + setDiffFilter(old => + changeFilterValue(old, 'branches', value, isChecked), + ); + }, }, { title: intl.formatMessage({ id: 'global.status' }), subtitle: intl.formatMessage({ id: 'filter.statusSubtitle' }), - items: Object.keys(statusObj).reduce((acc, k) => { - const newKey = k == 'TRUE' ? 'valid' : 'invalid'; - acc[newKey] = statusObj[k]; - return acc; - }, {} as TFilterApplied), - onClickItem: (status: string, isChecked: boolean) => - (statusObj[status == 'valid' ? 'TRUE' : 'FALSE'] = isChecked), + items: filter.status, + onClickItem: (value: string, isChecked: boolean): void => { + setDiffFilter(old => + changeFilterValue(old, 'status', value, isChecked), + ); + }, }, { title: intl.formatMessage({ id: 'global.configs' }), subtitle: intl.formatMessage({ id: 'filter.configsSubtitle' }), - items: configObj, - onClickItem: (config: string, isChecked: boolean) => - (configObj[config] = isChecked), + items: filter.configs, + onClickItem: (value: string, isChecked: boolean): void => { + setDiffFilter(old => + changeFilterValue(old, 'configs', value, isChecked), + ); + }, }, { title: intl.formatMessage({ id: 'global.architecture' }), subtitle: intl.formatMessage({ id: 'filter.architectureSubtitle' }), - items: archObj, - onClickItem: (arch: string, isChecked: boolean) => - (archObj[arch] = isChecked), + items: filter.archs, + onClickItem: (value: string, isChecked: boolean): void => { + setDiffFilter(old => + changeFilterValue(old, 'archs', value, isChecked), + ); + }, }, ]; - }, [statusObj, branchObj, configObj, archObj, intl]); + }, [intl, filter]); const checkboxSectionsComponents = useMemo( () => @@ -118,6 +169,8 @@ const TreeDetailsFilter = ({ export default TreeDetailsFilter; +const treeUrl = + 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git'; const summarySectionProps = { title: 'Tree', columns: [ From 686f53f5b6b78c4fef5958a15eb220da32a7c871 Mon Sep 17 00:00:00 2001 From: lfjnascimento Date: Tue, 23 Jul 2024 12:23:05 -0300 Subject: [PATCH 4/6] feat: add TreeDetailsFilterList to TreeDetails page --- dashboard/src/components/Tabs/Tabs.tsx | 9 ++++++++- dashboard/src/components/Tabs/TreeDetailsTab.tsx | 11 +++++++++-- dashboard/src/routes/TreeDetails/TreeDetails.tsx | 11 ++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/dashboard/src/components/Tabs/Tabs.tsx b/dashboard/src/components/Tabs/Tabs.tsx index a2bec9f8..d7983da7 100644 --- a/dashboard/src/components/Tabs/Tabs.tsx +++ b/dashboard/src/components/Tabs/Tabs.tsx @@ -15,9 +15,14 @@ export interface ITabItem { export interface ITabsComponent { tabs: ITabItem[]; defaultTab: ITabItem; + filterListElement?: JSX.Element; } -const TabsComponent = ({ defaultTab, tabs }: ITabsComponent): JSX.Element => { +const TabsComponent = ({ + defaultTab, + tabs, + filterListElement, +}: ITabsComponent): JSX.Element => { const tabsTrigger = useMemo( () => tabs.map(tab => ( @@ -48,6 +53,8 @@ const TabsComponent = ({ defaultTab, tabs }: ITabsComponent): JSX.Element => { return ( {tabsTrigger} +
{filterListElement}
+ {tabsContent}
); diff --git a/dashboard/src/components/Tabs/TreeDetailsTab.tsx b/dashboard/src/components/Tabs/TreeDetailsTab.tsx index 084f3e99..987d7c11 100644 --- a/dashboard/src/components/Tabs/TreeDetailsTab.tsx +++ b/dashboard/src/components/Tabs/TreeDetailsTab.tsx @@ -5,10 +5,12 @@ import TreeDetailsBuildTab from './TreeDetails/TreeDetailsBuildTab'; export interface ITreeDetailsBuildTab { treeDetailsData?: ITreeDetails; + filterListElement?: JSX.Element; } const TreeDetailsTab = ({ treeDetailsData, + filterListElement, }: ITreeDetailsBuildTab): JSX.Element => { const buildsTab: ITabItem = { name: 'treeDetails.builds', @@ -29,8 +31,13 @@ const TreeDetailsTab = ({ }; const treeDetailsTab = [buildsTab, bootsTab, testsTab]; - - return ; + return ( + + ); }; export default TreeDetailsTab; diff --git a/dashboard/src/routes/TreeDetails/TreeDetails.tsx b/dashboard/src/routes/TreeDetails/TreeDetails.tsx index 353fc584..1ebc8c5c 100644 --- a/dashboard/src/routes/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/routes/TreeDetails/TreeDetails.tsx @@ -14,6 +14,7 @@ import TreeDetailsFilter, { TFilter, } from './TreeDetailsFilter'; +import TreeDetailsFilterList from './TreeDetailsFilterList'; export interface ITreeDetails { archs: ISummaryItem[]; @@ -35,6 +36,11 @@ const TreeDetails = (): JSX.Element => { setFilter(createFilter(data)); } + const filterListElement = useMemo( + () => , + [filter], + ); + useEffect(() => { if (data) { const configsData: IListingItem[] = Object.entries( @@ -96,7 +102,10 @@ const TreeDetails = (): JSX.Element => {
- +
); From 2d16b6bcdcb0227c5420898912624ce0b762bb12 Mon Sep 17 00:00:00 2001 From: lfjnascimento Date: Tue, 23 Jul 2024 16:21:11 -0300 Subject: [PATCH 5/6] refactor: remove Refresh button from Drawer --- dashboard/src/components/Filter/Drawer.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/dashboard/src/components/Filter/Drawer.tsx b/dashboard/src/components/Filter/Drawer.tsx index a15f7170..54c586a4 100644 --- a/dashboard/src/components/Filter/Drawer.tsx +++ b/dashboard/src/components/Filter/Drawer.tsx @@ -16,7 +16,6 @@ import { Separator } from '../ui/separator'; interface IDrawerLink { treeURL: string; - onRefresh?: () => void; } interface IFilterDrawer extends IDrawerLink { @@ -41,10 +40,10 @@ const DrawerHeader = (): JSX.Element => { ); }; -const DrawerLink = ({ treeURL, onRefresh }: IDrawerLink): JSX.Element => { +const DrawerLink = ({ treeURL }: IDrawerLink): JSX.Element => { return (
-
+
@@ -56,13 +55,6 @@ const DrawerLink = ({ treeURL, onRefresh }: IDrawerLink): JSX.Element => { {treeURL}
-
); }; @@ -70,7 +62,6 @@ const DrawerLink = ({ treeURL, onRefresh }: IDrawerLink): JSX.Element => { const Drawer = ({ treeURL, children, - onRefresh, onCancel, onFilter, }: IFilterDrawer): JSX.Element => { @@ -85,7 +76,7 @@ const Drawer = ({
- +
{React.Children.map(children, (child, idx) => ( <> From 777bc553408de31e1c0ecd4982bc95fca0648b2e Mon Sep 17 00:00:00 2001 From: lfjnascimento Date: Tue, 23 Jul 2024 16:37:01 -0300 Subject: [PATCH 6/6] fix: Drawer should not apply any filters if cancel is cliked - also add TreeUl as prop --- .../src/routes/TreeDetails/TreeDetails.tsx | 21 +++++++++++++++++-- .../routes/TreeDetails/TreeDetailsFilter.tsx | 14 ++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/dashboard/src/routes/TreeDetails/TreeDetails.tsx b/dashboard/src/routes/TreeDetails/TreeDetails.tsx index 1ebc8c5c..48e0031a 100644 --- a/dashboard/src/routes/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/routes/TreeDetails/TreeDetails.tsx @@ -41,6 +41,19 @@ const TreeDetails = (): JSX.Element => { [filter], ); + //TODO: at some point `treeUrl` should be returned in `data` + const treeUrl = useMemo(() => { + let url = ''; + if (!data) return ''; + Object.entries(data.builds).some(([, build]) => { + if (build.git_repository_url) { + url = build.git_repository_url; + return true; + } + }); + return url; + }, [data]); + useEffect(() => { if (data) { const configsData: IListingItem[] = Object.entries( @@ -100,11 +113,15 @@ const TreeDetails = (): JSX.Element => {
- +
diff --git a/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx index 2f1d8588..3fea620a 100644 --- a/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx +++ b/dashboard/src/routes/TreeDetails/TreeDetailsFilter.tsx @@ -16,6 +16,7 @@ export type TFilter = interface ITreeDetailsFilter { filter: TFilter; onFilter: (filter: TFilter) => void; + treeUrl: string; } export type TFilterKeys = (typeof filterFieldMap)[keyof typeof filterFieldMap]; @@ -87,6 +88,7 @@ const changeFilterValue = ( const TreeDetailsFilter = ({ filter, onFilter, + treeUrl, }: ITreeDetailsFilter): JSX.Element => { const intl = useIntl(); const [diffFilter, setDiffFilter] = useState({}); @@ -106,6 +108,10 @@ const TreeDetailsFilter = ({ setDiffFilter({}); }, [filter, diffFilter, onFilter]); + const onClickCancel = useCallback(() => { + setDiffFilter({}); + }, []); + const checkboxSectionsProps: ICheckboxSection[] = useMemo(() => { return [ { @@ -160,7 +166,11 @@ const TreeDetailsFilter = ({ ); return ( - + {checkboxSectionsComponents} @@ -169,8 +179,6 @@ const TreeDetailsFilter = ({ export default TreeDetailsFilter; -const treeUrl = - 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git'; const summarySectionProps = { title: 'Tree', columns: [