diff --git a/dashboard/public/kernelci-logo-card.png b/dashboard/public/kernelci-logo-card.png new file mode 100644 index 000000000..4350b4af8 Binary files /dev/null and b/dashboard/public/kernelci-logo-card.png differ diff --git a/dashboard/src/components/BuildDetails/BuildDetails.tsx b/dashboard/src/components/BuildDetails/BuildDetails.tsx index 6d868dbf2..2025e8863 100644 --- a/dashboard/src/components/BuildDetails/BuildDetails.tsx +++ b/dashboard/src/components/BuildDetails/BuildDetails.tsx @@ -2,7 +2,7 @@ import { useIntl } from 'react-intl'; import { ErrorBoundary } from 'react-error-boundary'; import { useCallback, useMemo, useState, type JSX } from 'react'; -import type { LinkProps } from '@tanstack/react-router'; +import { useParams, type LinkProps } from '@tanstack/react-router'; import SectionGroup from '@/components/Section/SectionGroup'; import type { ISection } from '@/components/Section/Section'; @@ -40,6 +40,8 @@ import { StatusIcon } from '@/components/Icons/StatusIcons'; import PageWithTitle from '@/components/PageWithTitle'; +import { MemoizedBuildDetailsOGTags } from '@/components/OpenGraphTags/BuildDetailsOGTags'; + import BuildDetailsTestSection from './BuildDetailsTestSection'; interface BuildDetailsProps { @@ -96,17 +98,20 @@ const BuildDetails = ({ [setSheetType], ); + const buildDetailsTitle: string = useMemo(() => { + if (data?.git_commit_name && data.config_name) { + return `${data.git_commit_name} • ${data.config_name}`; + } + return data?.git_commit_name ?? data?.config_name ?? buildId; + }, [buildId, data]); + const generalSections: ISection[] = useMemo(() => { if (!data) { return []; } return [ { - title: valueOrEmpty( - data.git_commit_name - ? `${data.git_commit_name} • ${data.config_name}` - : data.config_name, - ), + title: buildDetailsTitle, leftIcon: , eyebrow: formatMessage({ id: 'buildDetails.buildDetails' }), subsections: [ @@ -205,7 +210,14 @@ const BuildDetails = ({ ], }, ]; - }, [data, formatMessage, hasUsefulLogInfo, buildId, setSheetToLog]); + }, [ + data, + buildDetailsTitle, + formatMessage, + buildId, + hasUsefulLogInfo, + setSheetToLog, + ]); const sectionsData: ISection[] = useMemo(() => { return [...generalSections, miscSection, filesSection].filter( @@ -213,15 +225,21 @@ const BuildDetails = ({ ); }, [generalSections, miscSection, filesSection]); - const buildTitle = `${data?.tree_name} ${data?.git_commit_name}`; + const buildDetailsTabTitle: string = useMemo(() => { + const buildTitle = `${data?.tree_name} ${data?.git_commit_name}`; + return formatMessage( + { id: 'title.buildDetails' }, + { buildName: getTitle(buildTitle, isLoading) }, + ); + }, [data?.git_commit_name, data?.tree_name, formatMessage, isLoading]); return ( - + + { + return getIssueCulprit({ + culprit_code: data?.culprit_code, + culprit_harness: data?.culprit_harness, + culprit_tool: data?.culprit_tool, + formatMessage: formatMessage, + }); + }, [ + data?.culprit_code, + data?.culprit_harness, + data?.culprit_tool, + formatMessage, + ]); + const generalSections: ISection[] = useMemo(() => { if (!data) { return []; @@ -151,13 +167,7 @@ export const IssueDetails = ({ }, { title: 'issueDetails.culpritTitle', - linkText: ( - - ), + linkText: issueCulprit, }, { title: 'issueDetails.id', @@ -172,7 +182,7 @@ export const IssueDetails = ({ ], }, ]; - }, [data, tagPills, formatMessage]); + }, [data, tagPills, formatMessage, issueCulprit]); const sectionsData: ISection[] = useMemo(() => { return [ @@ -183,13 +193,21 @@ export const IssueDetails = ({ ].filter(section => !!section); }, [generalSections, logspecSection, miscSection, firstIncidentSection]); + const issueDetailsTabTitle = useMemo(() => { + return formatMessage( + { id: 'title.issueDetails' }, + { issueName: getTitle(data?.comment, isLoading) }, + ); + }, [data?.comment, formatMessage, isLoading]); + return ( - + + { + const { formatMessage } = useIntl(); + + const buildDetailsDescription: string = useMemo(() => { + if (!data) { + return formatMessage({ id: 'buildDetails.buildDetails' }); + } + + const statusDescription = + formatMessage({ id: 'global.status' }) + + ': ' + + getBuildStatus(data.valid).toUpperCase(); + + const treeDescription = + formatMessage({ id: 'global.treeBranch' }) + + ': ' + + data.tree_name + + ' / ' + + data.git_repository_branch; + + const descriptionChunks = [ + descriptionTitle, + statusDescription, + treeDescription, + ]; + + return descriptionChunks.join(';\n'); + }, [descriptionTitle, data, formatMessage]); + + return ( + + ); +}; + +export const MemoizedBuildDetailsOGTags = memo(BuildDetailsOGTags); diff --git a/dashboard/src/components/OpenGraphTags/IssueDetailsOGTags.tsx b/dashboard/src/components/OpenGraphTags/IssueDetailsOGTags.tsx new file mode 100644 index 000000000..d5ba30dcc --- /dev/null +++ b/dashboard/src/components/OpenGraphTags/IssueDetailsOGTags.tsx @@ -0,0 +1,51 @@ +import { memo, useMemo, type JSX } from 'react'; + +import { useIntl } from 'react-intl'; + +import type { TIssueDetails } from '@/types/issueDetails'; + +import { OpenGraphTags } from './OpenGraphTags'; + +const IssueDetailsOGTags = ({ + title, + issueCulprit, + issueId, + data, +}: { + title: string; + issueCulprit: string; + issueId: string; + data?: TIssueDetails; +}): JSX.Element => { + const { formatMessage } = useIntl(); + + const issueDetailsDescription: string = useMemo(() => { + if (!data) { + return formatMessage({ id: 'issueDetails.issueDetails' }); + } + const versionDescription = + formatMessage({ id: 'issueDetails.version' }) + ': ' + data?.version; + + const culpritDescription = + formatMessage({ id: 'issueDetails.culpritTitle' }) + ': ' + issueCulprit; + + const firstSeen = data.extra?.[issueId]?.first_incident.first_seen; + const firstSeenDescription = firstSeen + ? formatMessage({ id: 'issue.firstSeen' }) + + ': ' + + new Date(firstSeen).toLocaleDateString() + : ''; + + const descriptionChunks = [ + versionDescription, + culpritDescription, + firstSeenDescription, + ].filter(chunk => chunk !== ''); + + return descriptionChunks.join(';\n'); + }, [data, formatMessage, issueCulprit, issueId]); + + return ; +}; + +export const MemoizedIssueDetailsOGTags = memo(IssueDetailsOGTags); diff --git a/dashboard/src/components/OpenGraphTags/ListingOGTags.tsx b/dashboard/src/components/OpenGraphTags/ListingOGTags.tsx new file mode 100644 index 000000000..d30278dec --- /dev/null +++ b/dashboard/src/components/OpenGraphTags/ListingOGTags.tsx @@ -0,0 +1,58 @@ +import type { JSX } from 'react'; +import { memo, useMemo } from 'react'; + +import { useIntl } from 'react-intl'; + +import type { PossibleMonitorPath } from '@/types/general'; +import type { MessagesKey } from '@/locales/messages'; + +import { OpenGraphTags } from './OpenGraphTags'; + +const ListingOGTags = ({ + monitor, + search, +}: { + monitor: PossibleMonitorPath; + search: string; +}): JSX.Element => { + const { formatMessage } = useIntl(); + + const listingDescription = useMemo(() => { + let descriptionId: MessagesKey; + + switch (monitor) { + case '/tree': + descriptionId = 'treeListing.description'; + break; + case '/hardware': + descriptionId = 'hardwareListing.description'; + break; + case '/issue': + descriptionId = 'issueListing.description'; + break; + } + return ( + formatMessage({ id: descriptionId }) + + (search !== '' + ? ';\n' + formatMessage({ id: 'global.search' }) + ': ' + search + : '') + ); + }, [formatMessage, monitor, search]); + + const listingTitle = useMemo(() => { + switch (monitor) { + case '/tree': + return formatMessage({ id: 'treeListing.title' }); + case '/hardware': + return formatMessage({ id: 'hardwareListing.title' }); + case '/issue': + return formatMessage({ id: 'issueListing.title' }); + } + }, [formatMessage, monitor]); + + return ( + + ); +}; + +export const MemoizedListingOGTags = memo(ListingOGTags); diff --git a/dashboard/src/components/OpenGraphTags/OpenGraphTags.tsx b/dashboard/src/components/OpenGraphTags/OpenGraphTags.tsx new file mode 100644 index 000000000..92902fc52 --- /dev/null +++ b/dashboard/src/components/OpenGraphTags/OpenGraphTags.tsx @@ -0,0 +1,29 @@ +import type { JSX } from 'react'; + +export const OpenGraphTags = ({ + title, + url, + description, + imageUrl = 'https://dashboard.kernelci.org/kernelci-logo-card.png', + type = 'website', +}: { + title: string; + url?: string; + description: string; + imageUrl?: string; + type?: string; +}): JSX.Element => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/dashboard/src/components/OpenGraphTags/TestDetailsOGTags.tsx b/dashboard/src/components/OpenGraphTags/TestDetailsOGTags.tsx new file mode 100644 index 000000000..c9e4aa70f --- /dev/null +++ b/dashboard/src/components/OpenGraphTags/TestDetailsOGTags.tsx @@ -0,0 +1,57 @@ +import type { JSX } from 'react'; +import { memo, useMemo } from 'react'; + +import { useIntl } from 'react-intl'; + +import type { TTestDetails } from '@/types/tree/TestDetails'; + +import { getTestHardware } from '@/lib/test'; + +import { OpenGraphTags } from './OpenGraphTags'; + +const TestDetailsOGTags = ({ + title, + data, +}: { + title: string; + data?: TTestDetails; +}): JSX.Element => { + const { formatMessage } = useIntl(); + + const testDetailsDescription: string = useMemo(() => { + if (!data) { + return formatMessage({ id: 'test.details' }); + } + + const statusDescription = + formatMessage({ id: 'global.status' }) + ': ' + data.status; + + const hardwareDescription = + formatMessage({ id: 'global.hardware' }) + + ': ' + + getTestHardware({ + misc: data.environment_misc, + compatibles: data.environment_compatible, + defaultValue: formatMessage({ id: 'global.unknown' }), + }); + + const treeDescription = + formatMessage({ id: 'global.treeBranch' }) + + ': ' + + data.tree_name + + ' / ' + + data.git_repository_branch; + + const descriptionChunks = [ + statusDescription, + treeDescription, + hardwareDescription, + ]; + + return descriptionChunks.join(';\n'); + }, [data, formatMessage]); + + return ; +}; + +export const MemoizedTestDetailsOGTags = memo(TestDetailsOGTags); diff --git a/dashboard/src/components/OpenGraphTags/TreeHardwareDetailsOGTags.tsx b/dashboard/src/components/OpenGraphTags/TreeHardwareDetailsOGTags.tsx new file mode 100644 index 000000000..23e20a3e0 --- /dev/null +++ b/dashboard/src/components/OpenGraphTags/TreeHardwareDetailsOGTags.tsx @@ -0,0 +1,90 @@ +import { useIntl, type IntlFormatters } from 'react-intl'; + +import { memo, useMemo, type JSX } from 'react'; + +import type { MessagesKey } from '@/locales/messages'; +import type { GroupedStatus } from '@/utils/status'; + +import { OpenGraphTags } from './OpenGraphTags'; + +const getCounterDescription = ({ + tabCount, + formatMessage, +}: { + tabCount: GroupedStatus; + formatMessage: IntlFormatters['formatMessage']; +}): string => { + const tabCounters: [MessagesKey, number][] = [ + ['tag.passCount', tabCount.successCount], + ['tag.failCount', tabCount.failedCount], + ['tag.inconclusiveCount', tabCount.inconclusiveCount], + ]; + const tabChunks: string[] = []; + tabCounters.map(([intlKey, count]) => { + if (count > 0) { + tabChunks.push(formatMessage({ id: intlKey }, { count: count })); + } + }); + + return tabChunks.join(', '); +}; + +const TreeHardwareDetailsOGTags = ({ + title, + buildCount, + bootCount, + testCount, +}: { + title: string; + buildCount: GroupedStatus; + bootCount: GroupedStatus; + testCount: GroupedStatus; +}): JSX.Element => { + const { formatMessage } = useIntl(); + + const treeDetailsDescription = useMemo(() => { + const allCounters: [MessagesKey, string][] = []; + + const buildDescription = getCounterDescription({ + tabCount: buildCount, + formatMessage: formatMessage, + }); + if (buildDescription.length > 0) { + allCounters.push(['global.builds', buildDescription]); + } + + const bootDescription = getCounterDescription({ + tabCount: bootCount, + formatMessage: formatMessage, + }); + if (bootDescription.length > 0) { + allCounters.push(['global.boots', bootDescription]); + } + + const testDescription = getCounterDescription({ + tabCount: testCount, + formatMessage: formatMessage, + }); + if (testDescription.length > 0) { + allCounters.push(['global.tests', testDescription]); + } + + if (allCounters.length > 0) { + const descriptionChunks: string[] = []; + allCounters.map(([intlKey, description]) => { + descriptionChunks.push( + formatMessage({ id: intlKey }) + ': ' + description, + ); + }); + return descriptionChunks.join(';\n'); + } + + return formatMessage({ id: 'tag.noBuildsOrTestsData' }); + }, [buildCount, bootCount, testCount, formatMessage]); + + return ; +}; + +export const MemoizedTreeHardwareDetailsOGTags = memo( + TreeHardwareDetailsOGTags, +); diff --git a/dashboard/src/components/OpenGraphTags/index.tsx b/dashboard/src/components/OpenGraphTags/index.tsx new file mode 100644 index 000000000..c0f33e033 --- /dev/null +++ b/dashboard/src/components/OpenGraphTags/index.tsx @@ -0,0 +1,3 @@ +import { OpenGraphTags } from './OpenGraphTags'; + +export { OpenGraphTags }; diff --git a/dashboard/src/components/Status/Status.tsx b/dashboard/src/components/Status/Status.tsx index 4aa1d2d33..a947ccb5b 100644 --- a/dashboard/src/components/Status/Status.tsx +++ b/dashboard/src/components/Status/Status.tsx @@ -3,6 +3,7 @@ import { Link, type LinkProps } from '@tanstack/react-router'; import type { JSX } from 'react'; import ColoredCircle from '@/components/ColoredCircle/ColoredCircle'; +import type { GroupedStatus } from '@/utils/status'; import { groupStatus } from '@/utils/status'; interface ITestStatus { @@ -15,6 +16,7 @@ interface ITestStatus { nullStatus?: number; forceNumber?: boolean; hideInconclusive?: boolean; + preCalculatedGroupedStatus?: GroupedStatus; } export const GroupedTestStatus = ({ @@ -26,20 +28,23 @@ export const GroupedTestStatus = ({ skip, nullStatus, hideInconclusive = false, + preCalculatedGroupedStatus, }: ITestStatus): JSX.Element => { - const { successCount, inconclusiveCount, failedCount } = groupStatus({ - doneCount: done, - errorCount: error, - failCount: fail, - missCount: miss, - passCount: pass, - skipCount: skip, - nullCount: nullStatus, - }); + const { successCount, inconclusiveCount, failedCount } = + preCalculatedGroupedStatus ?? + groupStatus({ + doneCount: done, + errorCount: error, + failCount: fail, + missCount: miss, + passCount: pass, + skipCount: skip, + nullCount: nullStatus, + }); return (
diff --git a/dashboard/src/components/TestDetails/TestDetails.tsx b/dashboard/src/components/TestDetails/TestDetails.tsx index f2a7089d0..5bcacb689 100644 --- a/dashboard/src/components/TestDetails/TestDetails.tsx +++ b/dashboard/src/components/TestDetails/TestDetails.tsx @@ -39,6 +39,9 @@ import { StatusIcon } from '@/components/Icons/StatusIcons'; import PageWithTitle from '@/components/PageWithTitle'; import { getTitle } from '@/utils/utils'; +import { getTestHardware } from '@/lib/test'; + +import { MemoizedTestDetailsOGTags } from '@/components/OpenGraphTags/TestDetailsOGTags'; const LinkItem = ({ children, ...props }: LinkProps): JSX.Element => { return ( @@ -55,27 +58,6 @@ const LinkItem = ({ children, ...props }: LinkProps): JSX.Element => { const MemoizedLinkItem = memo(LinkItem); -const getTestHardware = ({ - misc, - compatibles, - defaultValue, -}: { - misc?: Record; - compatibles?: string[]; - defaultValue?: string; -}): string => { - const platform = misc?.['platform']; - if (typeof platform === 'string' && platform !== '') { - return platform; - } - - if (compatibles && compatibles.length > 0) { - return compatibles[0]; - } - - return defaultValue ?? '-'; -}; - const TestDetailsSections = ({ test, setSheetType, @@ -336,13 +318,16 @@ const TestDetails = ({ breadcrumb }: TestsDetailsProps): JSX.Element => { const [sheetType, setSheetType] = useState('log'); const [jsonContent, setJsonContent] = useState(); + const testDetailsTabTitle: string = useMemo(() => { + return formatMessage( + { id: 'title.testDetails' }, + { testName: getTitle(data?.path, isLoading) }, + ); + }, [data?.path, formatMessage, isLoading]); + return ( - + + { + const result: string[] = []; + if (culprit_code) { + result.push(formatMessage({ id: 'issueDetails.culpritCode' })); + } + if (culprit_harness) { + result.push(formatMessage({ id: 'issueDetails.culpritHarness' })); + } + if (culprit_tool) { + result.push(formatMessage({ id: 'issueDetails.culpritTool' })); + } + + return valueOrEmpty(result.join(', ')); +}; diff --git a/dashboard/src/lib/test.ts b/dashboard/src/lib/test.ts new file mode 100644 index 000000000..7ec7655b4 --- /dev/null +++ b/dashboard/src/lib/test.ts @@ -0,0 +1,20 @@ +export const getTestHardware = ({ + misc, + compatibles, + defaultValue, +}: { + misc?: Record; + compatibles?: string[]; + defaultValue?: string; +}): string => { + const platform = misc?.['platform']; + if (typeof platform === 'string' && platform !== '') { + return platform; + } + + if (compatibles && compatibles.length > 0) { + return compatibles[0]; + } + + return defaultValue ?? '-'; +}; diff --git a/dashboard/src/locales/messages/index.ts b/dashboard/src/locales/messages/index.ts index fd7391e27..f22b2d4b6 100644 --- a/dashboard/src/locales/messages/index.ts +++ b/dashboard/src/locales/messages/index.ts @@ -201,6 +201,8 @@ export const messages = { 'hardwareDetails.platforms': 'Platforms', 'hardwareDetails.timeFrame': 'Results from {startDate} and {startTime} to {endDate} {endTime}', + 'hardwareListing.description': 'List of hardware from kernel tests', + 'hardwareListing.title': 'Hardware Listing ― KCI Dashboard', 'issue.alsoPresentTooltip': 'Issue also present in {tree}', 'issue.firstSeen': 'First seen', 'issue.newIssue': 'New issue: This is the first time this issue was seen', @@ -224,6 +226,8 @@ export const messages = { 'issueDetails.reportSubject': 'Report Subject', 'issueDetails.reportUrl': 'Report URL', 'issueDetails.version': 'Version', + 'issueListing.description': 'List of issues from builds and tests', + 'issueListing.title': 'Issue Listing ― KCI Dashboard', 'issueListing.treeBranchTooltip': 'The tree name and git repository branch of the first incident\nClick a cell to see details of that checkout', 'jsonSheet.title': 'JSON Viewer', @@ -250,6 +254,10 @@ export const messages = { 'table.itemsPerPage': 'Items per page:', 'table.of': 'of', 'table.showing': 'Showing:', + 'tag.failCount': '{count} Fail', + 'tag.inconclusiveCount': '{count} Inconclusive', + 'tag.noBuildsOrTestsData': 'No builds or tests data.', + 'tag.passCount': '{count} Pass', 'test.details': 'Test Details', 'test.statusTooltip': 'Success - tests with PASS status{br}' + @@ -294,6 +302,8 @@ export const messages = { 'treeDetails.testsInconclusive': 'Inconclusive tests', 'treeDetails.testsSuccess': 'Success tests', 'treeDetails.validBuilds': 'Success builds', + 'treeListing.description': 'List of trees for kernel builds and tests', + 'treeListing.title': 'Tree Listing ― KCI Dashboard', }, }; diff --git a/dashboard/src/pages/Hardware/Hardware.tsx b/dashboard/src/pages/Hardware/Hardware.tsx index 6c2a5926e..e61e419df 100644 --- a/dashboard/src/pages/Hardware/Hardware.tsx +++ b/dashboard/src/pages/Hardware/Hardware.tsx @@ -8,6 +8,7 @@ import { useNavigate, useSearch } from '@tanstack/react-router'; import HardwareListingPage from '@/pages/Hardware/HardwareListingPage'; import DebounceInput from '@/components/DebounceInput/DebounceInput'; +import { MemoizedListingOGTags } from '@/components/OpenGraphTags/ListingOGTags'; const Hardware = (): JSX.Element => { const { hardwareSearch } = useSearch({ @@ -29,10 +30,11 @@ const Hardware = (): JSX.Element => { [navigate], ); - const intl = useIntl(); + const { formatMessage } = useIntl(); return ( <> +
{ className="w-2/3" type="text" startingValue={hardwareSearch} - placeholder={intl.formatMessage({ + placeholder={formatMessage({ id: 'hardware.searchPlaceholder', })} /> diff --git a/dashboard/src/pages/IssueListing/IssueListing.tsx b/dashboard/src/pages/IssueListing/IssueListing.tsx index f66315444..1cf33e88e 100644 --- a/dashboard/src/pages/IssueListing/IssueListing.tsx +++ b/dashboard/src/pages/IssueListing/IssueListing.tsx @@ -9,6 +9,8 @@ import { z } from 'zod'; import DebounceInput from '@/components/DebounceInput/DebounceInput'; +import { MemoizedListingOGTags } from '@/components/OpenGraphTags/ListingOGTags'; + import { IssueListingPage } from './IssueListingPage'; const IssueListing = (): JSX.Element => { @@ -36,6 +38,7 @@ const IssueListing = (): JSX.Element => { return ( <> +
{ - const { valid, invalid } = data?.summary.builds.status ?? {}; + const [buildStatusCount, bootStatusCount, testStatusCount]: [ + GroupedStatus, + GroupedStatus, + GroupedStatus, + ] = useMemo(() => { + const { status: buildStatusSummary } = data?.summary.builds ?? {}; const { status: testStatusSummary } = data?.summary.tests ?? {}; - const { status: bootStatusSummary } = data?.summary.boots ?? {}; + const buildCount = groupStatus({ + passCount: buildStatusSummary?.valid, + failCount: buildStatusSummary?.invalid, + nullCount: buildStatusSummary?.null, + }); + + const bootCount = groupStatus({ + passCount: bootStatusSummary?.PASS, + failCount: bootStatusSummary?.FAIL, + doneCount: bootStatusSummary?.DONE, + errorCount: bootStatusSummary?.ERROR, + missCount: bootStatusSummary?.MISS, + skipCount: bootStatusSummary?.SKIP, + nullCount: bootStatusSummary?.NULL, + }); + + const testCount = groupStatus({ + passCount: testStatusSummary?.PASS, + failCount: testStatusSummary?.FAIL, + doneCount: testStatusSummary?.DONE, + errorCount: testStatusSummary?.ERROR, + missCount: testStatusSummary?.MISS, + skipCount: testStatusSummary?.SKIP, + nullCount: testStatusSummary?.NULL, + }); + + return [buildCount, bootCount, testCount]; + }, [data?.summary.boots, data?.summary.builds, data?.summary.tests]); + + const tabsCounts: TreeDetailsTabRightElement = useMemo(() => { return { - 'global.tests': testStatusSummary ? ( + 'global.tests': ( - ) : ( - <> ), - 'global.boots': bootStatusSummary ? ( + 'global.boots': ( - ) : ( - <> ), - 'global.builds': data ? ( - - ) : ( - <> ), }; - }, [data]); + }, [bootStatusCount, buildStatusCount, testStatusCount]); + + const treeDetailsTitle = formatMessage( + { id: 'title.treeDetails' }, + { + treeName: getTreeName(treeId, treeInfo.treeName, treeInfo.gitBranch), + }, + ); return ( - + + { const { treeSearch: unsafeTreeSearch } = useSearch({ @@ -32,10 +33,11 @@ const Trees = (): JSX.Element => { [navigate], ); - const intl = useIntl(); + const { formatMessage } = useIntl(); return ( <> +
{ className="w-2/3" type="text" startingValue={treeSearch} - placeholder={intl.formatMessage({ id: 'tree.searchPlaceholder' })} + placeholder={formatMessage({ id: 'tree.searchPlaceholder' })} />
diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx index 787977c6b..ce54dd217 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx @@ -29,10 +29,7 @@ import type { import MemoizedCompatibleHardware from '@/components/Cards/CompatibleHardware'; -import { - GroupedTestStatus, - BuildStatus as BuildStatusComponent, -} from '@/components/Status/Status'; +import { GroupedTestStatus } from '@/components/Status/Status'; import { mapFilterToReq } from '@/components/Tabs/Filters'; @@ -52,10 +49,13 @@ import { useHardwareDetailsLazyLoadQuery } from '@/hooks/useHardwareDetailsLazyL import { useQueryInconsistencyInvalidator } from '@/hooks/useQueryInconsistencyInvalidator'; -import { statusCountToRequiredStatusCount } from '@/utils/status'; +import type { GroupedStatus } from '@/utils/status'; +import { groupStatus, statusCountToRequiredStatusCount } from '@/utils/status'; import PageWithTitle from '@/components/PageWithTitle'; +import { MemoizedTreeHardwareDetailsOGTags } from '@/components/OpenGraphTags/TreeHardwareDetailsOGTags'; + import { HardwareHeader } from './HardwareDetailsHeaderTable'; import type { TreeDetailsTabRightElement } from './Tabs/HardwareDetailsTabs'; import HardwareDetailsTabs from './Tabs/HardwareDetailsTabs'; @@ -240,7 +240,11 @@ function HardwareDetails(): JSX.Element { const startDate = getFormattedDate(startTimestampInSeconds); const endDate = getFormattedDate(endTimestampInSeconds); - const tabsCounts: TreeDetailsTabRightElement = useMemo(() => { + const [buildStatusCount, bootStatusCount, testStatusCount]: [ + GroupedStatus, + GroupedStatus, + GroupedStatus, + ] = useMemo(() => { const { status: buildStatusSummary } = summaryResponse.data?.summary.builds ?? {}; const { status: testStatusSummary } = @@ -248,40 +252,63 @@ function HardwareDetails(): JSX.Element { const { status: bootStatusSummary } = summaryResponse.data?.summary.boots ?? {}; + const buildCount = groupStatus({ + passCount: buildStatusSummary?.valid, + failCount: buildStatusSummary?.invalid, + nullCount: buildStatusSummary?.null, + }); + + const bootCount = groupStatus({ + passCount: bootStatusSummary?.PASS, + failCount: bootStatusSummary?.FAIL, + doneCount: bootStatusSummary?.DONE, + errorCount: bootStatusSummary?.ERROR, + missCount: bootStatusSummary?.MISS, + skipCount: bootStatusSummary?.SKIP, + nullCount: bootStatusSummary?.NULL, + }); + + const testCount = groupStatus({ + passCount: testStatusSummary?.PASS, + failCount: testStatusSummary?.FAIL, + doneCount: testStatusSummary?.DONE, + errorCount: testStatusSummary?.ERROR, + missCount: testStatusSummary?.MISS, + skipCount: testStatusSummary?.SKIP, + nullCount: testStatusSummary?.NULL, + }); + + return [buildCount, bootCount, testCount]; + }, [ + summaryResponse.data?.summary.boots, + summaryResponse.data?.summary.builds, + summaryResponse.data?.summary.tests, + ]); + + const tabsCounts: TreeDetailsTabRightElement = useMemo(() => { return { - 'global.tests': testStatusSummary ? ( + 'global.tests': ( - ) : ( - <> ), - 'global.boots': bootStatusSummary ? ( + + 'global.boots': ( - ) : ( - <> ), - 'global.builds': buildStatusSummary ? ( - - ) : ( - <> ), }; - }, [ - summaryResponse.data?.summary.boots, - summaryResponse.data?.summary.builds, - summaryResponse.data?.summary.tests, - ]); + }, [bootStatusCount, buildStatusCount, testStatusCount]); const treeData = useMemo( () => @@ -301,13 +328,21 @@ function HardwareDetails(): JSX.Element { ], ); + const hardwareTitle = useMemo(() => { + return formatMessage( + { id: 'title.hardwareDetails' }, + { hardwareName: hardwareId }, + ); + }, [formatMessage, hardwareId]); + return ( - + +