diff --git a/backend/kernelCI_app/constants/hardwareDetails.py b/backend/kernelCI_app/constants/hardwareDetails.py index 4cd69fef5..699675b57 100644 --- a/backend/kernelCI_app/constants/hardwareDetails.py +++ b/backend/kernelCI_app/constants/hardwareDetails.py @@ -1 +1,9 @@ +import hashlib + SELECTED_HEAD_TREE_VALUE = "head" +TREE_KEY_HASH_LENGTH = 14 + + +def make_tree_key(tree_name: str, branch: str, url: str) -> str: + raw = f"{tree_name}|{branch}|{url}" + return hashlib.sha256(raw.encode()).hexdigest()[:TREE_KEY_HASH_LENGTH] diff --git a/backend/kernelCI_app/constants/localization.py b/backend/kernelCI_app/constants/localization.py index 190ce2dd2..6f32fa8bc 100644 --- a/backend/kernelCI_app/constants/localization.py +++ b/backend/kernelCI_app/constants/localization.py @@ -99,7 +99,17 @@ class DocStrings: HARDWARE_DETAILS_ORIGIN_DESCRIPTION = "Origin of the tests of the hardware" HARDWARE_DETAILS_SEL_COMMITS_DESCRIPTION = ( - "Dictionary mapping tree names to selected commit hashes" + "Dictionary mapping stable tree keys to selected commit hashes. " + "Keys are the first 14 hexadecimal characters of SHA-256 over " + "'tree_name|git_repository_branch|git_repository_url', matching " + "Tree.index values returned by the hardware details endpoints. " + "Values are a git commit hash or 'head' to use the tree head commit. " + "An empty object selects all trees with their head commits." + ) + HARDWARE_DETAILS_TREE_INDEX_DESCRIPTION = ( + "Stable identifier for a tree, derived as the first 14 hexadecimal " + "characters of SHA-256 over " + "'tree_name|git_repository_branch|git_repository_url'" ) HARDWARE_LISTING_ORIGIN_DESCRIPTION = "Origin of the hardware" diff --git a/backend/kernelCI_app/helpers/hardwareDetails.py b/backend/kernelCI_app/helpers/hardwareDetails.py index 028408aef..33884674f 100644 --- a/backend/kernelCI_app/helpers/hardwareDetails.py +++ b/backend/kernelCI_app/helpers/hardwareDetails.py @@ -11,9 +11,7 @@ UNCATEGORIZED_STRING, UNKNOWN_STRING, ) -from kernelCI_app.constants.hardwareDetails import ( - SELECTED_HEAD_TREE_VALUE, -) +from kernelCI_app.constants.hardwareDetails import SELECTED_HEAD_TREE_VALUE from kernelCI_app.helpers.commonDetails import PossibleTabs, add_unfiltered_issue from kernelCI_app.helpers.filters import ( FilterParams, @@ -98,9 +96,7 @@ def get_trees_with_selected_commit( selected: List[Tree] = [] for tree in trees: - tree_idx = tree.index - - raw_selected_commit = selected_commits.get(tree_idx) + raw_selected_commit = selected_commits.get(tree.index) is_tree_selected = raw_selected_commit is not None diff --git a/backend/kernelCI_app/queries/hardware.py b/backend/kernelCI_app/queries/hardware.py index 422b487e6..57df1b7cb 100644 --- a/backend/kernelCI_app/queries/hardware.py +++ b/backend/kernelCI_app/queries/hardware.py @@ -4,6 +4,7 @@ from django.db import connection, connections from kernelCI_app.cache import get_query_cache, set_query_cache +from kernelCI_app.constants.hardwareDetails import make_tree_key from kernelCI_app.helpers.database import dict_fetchall from kernelCI_app.queries.duration import ( get_boot_test_duration_clause, @@ -833,6 +834,8 @@ def get_hardware_trees_head_commits( query = _get_hardware_trees_from_status_query( fields=""" C.tree_name, + C.git_repository_branch, + C.git_repository_url, C.git_commit_hash """ ) @@ -847,8 +850,15 @@ def get_hardware_trees_head_commits( cursor.execute(query, params) tree_records = dict_fetchall(cursor) trees = [ - (str(idx), tree["git_commit_hash"]) - for (idx, tree) in enumerate(tree_records) + ( + make_tree_key( + tree["tree_name"] or "", + tree["git_repository_branch"] or "", + tree["git_repository_url"] or "", + ), + tree["git_commit_hash"], + ) + for tree in tree_records ] set_query_cache(key=cache_key, params=cache_params, rows=trees) @@ -890,10 +900,15 @@ def get_hardware_trees_data( tree_records = dict_fetchall(cursor) trees = [] - for idx, tree in enumerate(tree_records): + for tree in tree_records: + tree_index = make_tree_key( + tree["tree_name"] or "", + tree["git_repository_branch"] or "", + tree["git_repository_url"] or "", + ) trees.append( Tree( - index=str(idx), + index=tree_index, tree_name=tree["tree_name"], origin=tree["origin"], git_repository_branch=tree["git_repository_branch"], diff --git a/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py b/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py index 8545c1421..5fa595d02 100644 --- a/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py +++ b/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py @@ -1,5 +1,6 @@ from collections import defaultdict +from kernelCI_app.constants.hardwareDetails import make_tree_key from kernelCI_app.typeModels.common import StatusCount from kernelCI_app.typeModels.commonDetails import ( TestArchSummaryItem, @@ -10,8 +11,13 @@ def create_tree(**overrides): """Create Tree.""" + tree_name = overrides.get("tree_name", "mainline") + branch = overrides.get("git_repository_branch", "master") + url = overrides.get("git_repository_url", "https://git.kernel.org") + default_index = make_tree_key(tree_name, branch, url) + base_tree = Tree( - index="1", + index=overrides.get("index", default_index), origin="test", tree_name="mainline", git_repository_branch="master", @@ -29,12 +35,16 @@ def create_tree(**overrides): return base_tree +BASE_TREE_KEY = make_tree_key("mainline", "master", "https://git.kernel.org") +DIFF_TREE_KEY = make_tree_key("stable", "linux-5.4.y", "https://git.kernel.org") + + def create_tree_status_summary(**overrides): """Create tree status summary.""" summary = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) - summary["1"]["builds"]["PASS"] = 5 - summary["2"]["builds"]["FAIL"] = 2 + summary[BASE_TREE_KEY]["builds"]["PASS"] = 5 + summary[DIFF_TREE_KEY]["builds"]["FAIL"] = 2 for tree_id, tree_data in overrides.items(): for category, status_data in tree_data.items(): @@ -127,7 +137,6 @@ def create_test_summary(**overrides): base_tree = create_tree() tree_with_different_commit = create_tree( - index="2", tree_name="stable", git_repository_branch="linux-5.4.y", head_git_commit_name="commit2", diff --git a/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py b/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py index f59e708c9..5058a3cb0 100644 --- a/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py +++ b/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py @@ -41,6 +41,8 @@ update_issues, ) from kernelCI_app.tests.unitTests.helpers.fixtures.hardware_details_data import ( + BASE_TREE_KEY, + DIFF_TREE_KEY, base_tree, base_tree_status_summary, create_test_summary, @@ -126,9 +128,9 @@ def test_get_displayed_commit_with_head_tree_value(self): class TestGetTreesWithSelectedCommit: def test_get_trees_with_selected_commit(self): - """Test get_trees_with_selected_commit function.""" + """Test get_trees_with_selected_commit function with stable keys.""" trees = [base_tree, tree_with_different_commit] - selected_commits = {"1": "custom123", "2": None} + selected_commits = {BASE_TREE_KEY: "custom123", DIFF_TREE_KEY: None} result = get_trees_with_selected_commit( trees=trees, selected_commits=selected_commits diff --git a/backend/kernelCI_app/typeModels/hardwareDetails.py b/backend/kernelCI_app/typeModels/hardwareDetails.py index 2cd459d2f..45d44ed80 100644 --- a/backend/kernelCI_app/typeModels/hardwareDetails.py +++ b/backend/kernelCI_app/typeModels/hardwareDetails.py @@ -79,7 +79,7 @@ class CommitHistoryValidCheckout(BaseModel): class Tree(BaseModel): - index: str + index: str = Field(description=DocStrings.HARDWARE_DETAILS_TREE_INDEX_DESCRIPTION) origin: Origin tree_name: Optional[str] git_repository_branch: Optional[str] diff --git a/backend/kernelCI_app/views/hardwareDetailsSummaryView.py b/backend/kernelCI_app/views/hardwareDetailsSummaryView.py index 71afccf36..8ec1c9706 100644 --- a/backend/kernelCI_app/views/hardwareDetailsSummaryView.py +++ b/backend/kernelCI_app/views/hardwareDetailsSummaryView.py @@ -13,6 +13,7 @@ from rest_framework.views import APIView from kernelCI_app.constants.general import UNKNOWN_STRING +from kernelCI_app.constants.hardwareDetails import make_tree_key from kernelCI_app.constants.localization import ClientStrings from kernelCI_app.helpers.errorHandling import create_api_error_response from kernelCI_app.helpers.filters import ( @@ -368,8 +369,12 @@ def aggregate_common( t.head_git_commit_hash or "", ), ) - for i, tree in enumerate(sorted_trees): - tree.index = str(i) + for tree in sorted_trees: + tree.index = make_tree_key( + tree.tree_name or "", + tree.git_repository_branch or "", + tree.git_repository_url or "", + ) return sorted_trees, sorted(all_compatibles) @@ -473,14 +478,14 @@ def valid_filter_status(self) -> bool: def select_commits_hashes( self, - tree_heads: list[(str, str)], + tree_heads: list[tuple[str, str]], selected_commits: Optional[dict[str, str]] = None, ): selected_commit_hashes = [] if selected_commits: - for idx, head in tree_heads: - if idx in self.selected_commits: - selected_commit = self.selected_commits.get(idx, "head") + for key, head in tree_heads: + if key in self.selected_commits: + selected_commit = self.selected_commits.get(key, "head") selected_commit_hashes.append( head if selected_commit == "head" else selected_commit ) diff --git a/backend/schema.yml b/backend/schema.yml index 6269c5c28..e37dcfafa 100644 --- a/backend/schema.yml +++ b/backend/schema.yml @@ -3194,7 +3194,11 @@ components: selectedCommits: additionalProperties: type: string - description: Dictionary mapping tree names to selected commit hashes + description: Dictionary mapping stable tree keys to selected commit hashes. + Keys are the first 14 hexadecimal characters of SHA-256 over 'tree_name|git_repository_branch|git_repository_url', + matching Tree.index values returned by the hardware details endpoints. + Values are a git commit hash or 'head' to use the tree head commit. An + empty object selects all trees with their head commits. title: Selectedcommits type: object filter: @@ -4763,6 +4767,8 @@ components: Tree: properties: index: + description: Stable identifier for a tree, derived as the first 14 hexadecimal + characters of SHA-256 over 'tree_name|git_repository_branch|git_repository_url' title: Index type: string origin: diff --git a/dashboard/src/api/hardwareDetails.ts b/dashboard/src/api/hardwareDetails.ts index 43d30a38f..f86452f53 100644 --- a/dashboard/src/api/hardwareDetails.ts +++ b/dashboard/src/api/hardwareDetails.ts @@ -21,26 +21,21 @@ import { RequestData } from './commonRequest'; const TREE_SELECT_HEAD_VALUE = 'head'; const mapIndexesToSelectedTrees = ( - selectedIndexes: number[] | null, - treeIndexesLength?: number, + selectedIndexes: string[] | null, treeCommits: TTreeCommits = {}, + treeKeys: string[] = [], ): Record => { const selectedTrees: Record = {}; - if (selectedIndexes?.length === 0 && isEmptyObject(treeCommits)) { - return selectedTrees; - } - - const selectedArray = - treeIndexesLength && - (selectedIndexes === null || selectedIndexes.length === 0) - ? Array.from({ length: treeIndexesLength }, (_, i) => i) + const keys = + selectedIndexes === null || selectedIndexes.length === 0 + ? isEmptyObject(treeCommits) + ? [] + : treeKeys : selectedIndexes; - selectedArray?.forEach(i => { - const key = i.toString(); - const value = treeCommits[key] || TREE_SELECT_HEAD_VALUE; - selectedTrees[key] = value; + keys.forEach(key => { + selectedTrees[key] = treeCommits[key] || TREE_SELECT_HEAD_VALUE; }); return selectedTrees; @@ -103,9 +98,9 @@ export type UseHardwareDetailsWithoutVariant = { endTimestampInSeconds: number; origin: string; filter: TFilter; - selectedIndexes: number[] | null; + selectedIndexes: string[] | null; treeCommits: TTreeCommits; - treeIndexesLength?: number; + treeKeys?: string[]; enabled?: boolean; }; @@ -121,7 +116,7 @@ export const useHardwareDetails = ({ filter, selectedIndexes, treeCommits, - treeIndexesLength, + treeKeys = [], enabled = true, variant, }: UseHardwareDetailsParameters): UseQueryResult< @@ -139,8 +134,8 @@ export const useHardwareDetails = ({ const selectedTrees = mapIndexesToSelectedTrees( selectedIndexes, - treeIndexesLength, treeCommits, + treeKeys, ); const body: fetchHardwareDetailsBody = { diff --git a/dashboard/src/components/Tabs/Filters.tsx b/dashboard/src/components/Tabs/Filters.tsx index 53ba502ea..97c8afcee 100644 --- a/dashboard/src/components/Tabs/Filters.tsx +++ b/dashboard/src/components/Tabs/Filters.tsx @@ -138,7 +138,7 @@ interface ICheckboxSectionProps extends SectionsProps { interface ITreeSectionProps { items?: Record; - selectedTrees?: number[]; + selectedTrees?: string[]; handleSelectTree: (index: string) => void; showIcon?: boolean; } @@ -276,9 +276,9 @@ const TreeSelectSection = ({ const filteredItems: Record = {}; Object.keys(items).map(key => { - const idx = Number(key.split('__')[1] ?? NO_VALID_INDEX); + const treeKey = key.split('__')[1] ?? ''; - filteredItems[key] = selectedTrees?.includes(idx) || false; + filteredItems[key] = selectedTrees?.includes(treeKey) || false; }); return filteredItems; diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx index 178f39cba..da0580f84 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx @@ -216,7 +216,7 @@ function HardwareDetails(): JSX.Element { }); }, [navigate]); - const [treeIndexesLength, setTreeIndexesLength] = useState(0); + const [treeKeys, setTreeKeys] = useState([]); const { summary: summaryResponse, full: fullResponse } = useHardwareDetailsLazyLoadQuery({ hardwareId: hardwareId, @@ -226,9 +226,19 @@ function HardwareDetails(): JSX.Element { filter: reqFilter, selectedIndexes: treeIndexes, treeCommits: treeCommits, - treeIndexesLength: treeIndexesLength, + treeKeys, }); + useEffect(() => { + const next = + summaryResponse.data?.common.trees.map(tree => tree.index) ?? []; + setTreeKeys(prev => + prev.length === next.length && prev.every((key, i) => key === next[i]) + ? prev + : next, + ); + }, [summaryResponse.data?.common.trees]); + const hardwareStatusHistoryState = useRouterState({ select: s => s.location.state.hardwareStatusCount, }); @@ -236,7 +246,7 @@ function HardwareDetails(): JSX.Element { const numIndexes = summaryResponse?.data?.common?.trees?.length || 0; const updateTreeFilters = useCallback( ( - selectedIndexes: number[] | null, + selectedIndexes: string[] | null, { replace = false }: { replace?: boolean } = {}, ) => { const numSelectedIndexes = selectedIndexes?.length || 0; @@ -348,7 +358,6 @@ function HardwareDetails(): JSX.Element { return; } - setTreeIndexesLength(trees.length); navigate({ search: prev => ({ ...prev, treeCommits: newTreeCommits }), state: s => s, @@ -361,7 +370,6 @@ function HardwareDetails(): JSX.Element { commitHistoryTable, commitHistoryIsLoading, navigate, - setTreeIndexesLength, ]); const filterListElement = useMemo(() => { @@ -588,7 +596,6 @@ function HardwareDetails(): JSX.Element { treeItems={treeData} selectedIndexes={treeIndexes} updateTreeFilters={updateTreeFilters} - setTreeIndexesLength={setTreeIndexesLength} selectionResetKey={`${hardwareId}\0${hardwareSearch ?? ''}`} /> {summaryResponse.data && diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx index d8db94ab5..821d64dd7 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx @@ -12,7 +12,6 @@ import { MemoizedCheckboxSection, MemoizedTimeRangeSection, MemoizedTreeSelectSection, - NO_VALID_INDEX, } from '@/components/Tabs/Filters'; import type { ISectionItem } from '@/components/Filter/CheckboxSection'; import { isTFilterObjectKeys, type TFilter } from '@/types/general'; @@ -27,11 +26,11 @@ interface IHardwareDetailsFilter { paramFilter: TFilter; hardwareName: string; data: HardwareDetailsSummary | undefined; - selectedTrees: number[] | null; + selectedTrees: string[] | null; } type TFilterCreate = TFilter & { - treeIndexes: number[]; + treeIndexes: string[]; }; export const createFilter = ( @@ -55,14 +54,14 @@ export const createFilter = ( const compilers: TFilterValues = {}; const trees: TFilterValues = {}; const compatibles: TFilterValues = {}; - const treeIndexes: number[] = []; + const treeIndexes: string[] = []; const labs: TFilterValues = {}; if (data) { // Global filters data.common.trees.forEach(tree => { - const treeIdx = Number(tree.index); + const treeKey = tree.index; const treeName = tree.tree_name ?? 'Unknown'; const treeBranch = tree.git_repository_branch ?? 'Unknown'; @@ -72,8 +71,8 @@ export const createFilter = ( treeNameBranch = 'Unknown'; } - trees[`${treeNameBranch}__${treeIdx}`] = true; - treeIndexes.push(treeIdx); + trees[`${treeNameBranch}__${treeKey}`] = true; + treeIndexes.push(treeKey); }); data.common.compatibles.forEach(c => (compatibles[c] = false)); @@ -222,7 +221,7 @@ const HardwareDetailsFilter = ({ }, [data]); const [diffFilter, setDiffFilter] = useState(paramFilter); - const [treeIndexes, setTreeIndexes] = useState([]); + const [treeIndexes, setTreeIndexes] = useState([]); useEffect(() => { const initialTrees = @@ -260,12 +259,12 @@ const HardwareDetailsFilter = ({ const handleSelectTree = useCallback( (value: string) => { - const idx = Number(value.split('__')[1] ?? NO_VALID_INDEX); + const key = value.split('__')[1] ?? ''; - if (treeIndexes.includes(idx)) { - setTreeIndexes(treeIndexes.filter(i => i !== idx)); + if (treeIndexes.includes(key)) { + setTreeIndexes(treeIndexes.filter(k => k !== key)); } else { - setTreeIndexes([...treeIndexes, idx]); + setTreeIndexes([...treeIndexes, key]); } }, [treeIndexes], diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetailsHeaderTable.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetailsHeaderTable.tsx index 45e205731..8a3cfad1e 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetailsHeaderTable.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetailsHeaderTable.tsx @@ -13,7 +13,7 @@ import { useReactTable, } from '@tanstack/react-table'; -import type { SetStateAction, Dispatch, JSX } from 'react'; +import type { JSX } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -55,12 +55,11 @@ const DEBOUNCE_INTERVAL = 2000; interface IHardwareHeader { treeItems: PreparedTrees[]; - selectedIndexes: number[] | null; + selectedIndexes: string[] | null; updateTreeFilters: ( - selectedIndexes: number[] | null, + selectedIndexes: string[] | null, options?: { replace?: boolean }, ) => void; - setTreeIndexesLength: Dispatch>; selectionResetKey: string; } @@ -71,9 +70,7 @@ const CommitSelector = ({ selectableCommits, isCommitsLoading, treeIndex, - rowLength, isMainPageLoading, - setTreeIndexesLength, }: { headCommitName?: string; headCommitHash?: string; @@ -82,19 +79,12 @@ const CommitSelector = ({ isCommitsLoading: boolean; isMainPageLoading: boolean; treeIndex: string; - rowLength: number; - setTreeIndexesLength: IHardwareHeader['setTreeIndexesLength']; }): JSX.Element => { const navigate = useNavigate({ from: '/hardware/$hardwareId/' }); const { treeCommits } = useSearch({ from: '/_main/hardware/$hardwareId' }); const navigateToThePast = useCallback( (commitHash: string) => { - if (treeIndex === null) { - return; - } - setTreeIndexesLength(rowLength); - const newTreeCommits = { ...treeCommits, [treeIndex]: commitHash }; if (commitHash === headCommitHash) { delete newTreeCommits[treeIndex]; @@ -110,14 +100,7 @@ const CommitSelector = ({ state: s => s, }); }, - [ - navigate, - setTreeIndexesLength, - headCommitHash, - rowLength, - treeIndex, - treeCommits, - ], + [navigate, headCommitHash, treeIndex, treeCommits], ); const gitValues = useMemo(() => { @@ -208,9 +191,7 @@ const CommitSelector = ({ ); }; -const getColumns = ( - setTreeIndexesLength: IHardwareHeader['setTreeIndexesLength'], -): ColumnDef[] => { +const getColumns = (): ColumnDef[] => { return [ { id: 'select', @@ -255,7 +236,7 @@ const getColumns = ( header: ({ column }): JSX.Element => ( ), - cell: ({ row, table }): JSX.Element => { + cell: ({ row }): JSX.Element => { return ( ); }, @@ -333,7 +312,7 @@ const getColumns = ( }; const getInitialRowSelection = ( - selectedIndexes: number[] | null, + selectedIndexes: string[] | null, treeItems: PreparedTrees[], isInitialLoad = false, ): Record => { @@ -352,34 +331,26 @@ const getInitialRowSelection = ( ); } + const selectedSet = new Set(selectedIndexes); return Object.fromEntries( - selectedIndexes - .map(idx => treeItems.find(t => t.index === String(idx))) - .filter((t): t is PreparedTrees => !!t) - .map(t => [t.index, true]), + treeItems.map(item => [item.index, selectedSet.has(item.index)]), ); }; const indexesFromRowSelection = ( rowSelection: RowSelectionState, -): number[] | null => { - const rowSelectionValues = Object.values(rowSelection); - if (rowSelectionValues.length === 0) { - return []; - } +): string[] | null => { + const selected = Object.entries(rowSelection) + .filter(([, isSelected]) => isSelected) + .map(([key]) => key); - const selectedIndexes = Object.keys(rowSelection).map(rowId => - parseInt(rowId), - ); - - return selectedIndexes; + return selected; }; export function HardwareHeader({ treeItems, selectedIndexes = null, updateTreeFilters, - setTreeIndexesLength, selectionResetKey, }: IHardwareHeader): JSX.Element { const [sorting, setSorting] = useState([ @@ -448,10 +419,7 @@ export function HardwareHeader({ [], ); - const columns = useMemo( - () => getColumns(setTreeIndexesLength), - [setTreeIndexesLength], - ); + const columns = useMemo(() => getColumns(), []); const table = useReactTable({ data: treeItems, diff --git a/dashboard/src/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph.tsx b/dashboard/src/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph.tsx index 8b9b0f446..700cfefa0 100644 --- a/dashboard/src/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph.tsx +++ b/dashboard/src/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph.tsx @@ -34,9 +34,13 @@ const HardwareCommitNavigationGraph = ({ [diffFilter, hardwareId], ); - const treeIdx = - trees.length === 1 ? 0 : treeIndexes?.length === 1 ? treeIndexes[0] : null; - const tree = treeIdx !== null && trees[treeIdx]; + const treeKey = + trees.length === 1 + ? trees[0].index + : treeIndexes?.length === 1 + ? treeIndexes[0] + : null; + const tree = treeKey !== null && trees.find(t => t.index === treeKey); const commitHeads = useMemo( (): CommitHead[] => @@ -61,7 +65,7 @@ const HardwareCommitNavigationGraph = ({ ); const commitsList = useMemo(() => { - const treeForIdentifier = treeIdx !== null ? trees[treeIdx] : undefined; + const treeForIdentifier = tree || undefined; const key = treeForIdentifier ? makeTreeIdentifierKey({ treeName: treeForIdentifier.tree_name ?? '', @@ -71,30 +75,30 @@ const HardwareCommitNavigationGraph = ({ : ''; const entries = commitHistoryData?.commit_history_table?.[key] ?? []; return entries.map(c => c.git_commit_hash); - }, [commitHistoryData?.commit_history_table, treeIdx, trees]); + }, [commitHistoryData?.commit_history_table, tree]); const markClickHandle = useCallback( (commitHash: string) => { - if (treeIdx === null) { + if (treeKey === null) { return; } navigate({ search: current => ({ ...current, - treeCommits: { ...treeCommits, [treeIdx]: commitHash }, + treeCommits: { ...treeCommits, [treeKey]: commitHash }, }), state: s => s, }); }, - [navigate, treeIdx, treeCommits], + [navigate, treeKey, treeCommits], ); if (!tree) { return <>; } - const treeId = treeCommits?.[treeIdx] ?? tree['head_git_commit_hash']; + const treeId = treeCommits?.[treeKey] ?? tree['head_git_commit_hash']; return ( values.map(String)) + .nullable() + .default(null), treeCommits: zTreeCommits, tableFilter: zTableFilterInfoValidator, diffFilter: zDiffFilter, diff --git a/dashboard/src/utils/search.test.ts b/dashboard/src/utils/search.test.ts index ffc0eb0a3..0c0bbaf18 100644 --- a/dashboard/src/utils/search.test.ts +++ b/dashboard/src/utils/search.test.ts @@ -17,20 +17,19 @@ const KEY_FLAT_CHAR = '|'; const simpleObject = { origin: 'maestro', intervalInDays: 7, - // eslint-disable-next-line no-magic-numbers - treeIndexes: [1, 2, 3], + treeIndexes: ['a3f2b8c1', 'b4c5d6e7', 'c5d6e7f8'], tableSort: '-status', }; const simpleObjectMinify = { o: 'maestro', i: 7, - // eslint-disable-next-line no-magic-numbers - x: [1, 2, 3], + x: ['a3f2b8c1', 'b4c5d6e7', 'c5d6e7f8'], s: '-status', }; -const simpleObjectStringify = '?o=maestro&i=7&x[]=1,2,3&s=-status'; +const simpleObjectStringify = + '?o=maestro&i=7&x[]=a3f2b8c1,b4c5d6e7,c5d6e7f8&s=-status'; const nestedObject = { origin: 'maestro', @@ -53,7 +52,7 @@ const nestedObject = { archs: { arm: true }, testPath: 'amlogic', }, - treeIndexes: [0, 1, 2], + treeIndexes: ['a3f2b8c1', 'b4c5d6e7', 'c5d6e7f8'], }; const nestedObjectMinify = { @@ -77,7 +76,7 @@ const nestedObjectMinify = { a: { arm: true }, tp: 'amlogic', }, - x: [0, 1, 2], + x: ['a3f2b8c1', 'b4c5d6e7', 'c5d6e7f8'], }; const nestedObjectStringify = @@ -86,7 +85,7 @@ const nestedObjectStringify = '&s|b=path&s|t=-startTime' + '&tri|t=android&tri|ch=hash' + '&df|c|defconfig=true&df|a|arm=true&df|tp=amlogic' + - '&x[]=0,1,2'; + '&x[]=a3f2b8c1,b4c5d6e7,c5d6e7f8'; const flatObject = { origin: 'maestro', @@ -101,7 +100,7 @@ const flatObject = { 'diffFilter|configs|defconfig': true, 'diffFilter|archs|arm': true, 'diffFilter|testPath': 'amlogic', - treeIndexes: [0, 1, 2], + treeIndexes: ['a3f2b8c1', 'b4c5d6e7', 'c5d6e7f8'], }; const flatObjectMinify = { @@ -117,7 +116,7 @@ const flatObjectMinify = { 'df|c|defconfig': true, 'df|a|arm': true, 'df|tp': 'amlogic', - x: [0, 1, 2], + x: ['a3f2b8c1', 'b4c5d6e7', 'c5d6e7f8'], }; describe('isEncodedArrayParam', () => { @@ -271,11 +270,13 @@ describe('parseSearch', () => { ); }); - it('JSON stringified simple object with filled array', () => { + it('JSON stringified simple object with filled numeric array (legacy)', () => { + // eslint-disable-next-line no-magic-numbers + const legacyObject = { ...simpleObject, treeIndexes: [1, 2, 3] }; const JSONSimpleObjectStringify = stringifySearchWith(JSON.stringify)( - simpleObject, + legacyObject, ); - expect(parseSearch(JSONSimpleObjectStringify)).toStrictEqual(simpleObject); + expect(parseSearch(JSONSimpleObjectStringify)).toStrictEqual(legacyObject); }); it('JSON stringified simple object with empty array', () => { @@ -288,15 +289,17 @@ describe('parseSearch', () => { ); }); - it('JSON stringified simple object - not URI encoded', () => { + it('JSON stringified simple object - not URI encoded (legacy)', () => { + // eslint-disable-next-line no-magic-numbers + const legacyObject = { ...simpleObject, treeIndexes: [1, 2, 3] }; const JSONSimpleObjectStringify = stringifySearchWith(JSON.stringify)( - simpleObject, + legacyObject, ); const decodeStr = JSONSimpleObjectStringify.replace(/%5B/g, '[').replace( /%5D/g, ']', ); - expect(parseSearch(decodeStr)).toStrictEqual(simpleObject); + expect(parseSearch(decodeStr)).toStrictEqual(legacyObject); }); it('JSON stringified nested object', () => { diff --git a/dashboard/src/utils/search.ts b/dashboard/src/utils/search.ts index bd2ae4741..050a28649 100644 --- a/dashboard/src/utils/search.ts +++ b/dashboard/src/utils/search.ts @@ -52,7 +52,7 @@ export const parseSearch = (searchStr: string): AnySchema => { df_btdm: 'number', df_tdx: 'number', df_tdm: 'number', - x: 'number[]', + x: 'string[]', st: 'number', et: 'number', iv: 'number',