From d038f08daa19591eafe932f042c6b71ae21f3c90 Mon Sep 17 00:00:00 2001 From: Marcelo Robert Santos Date: Tue, 25 Feb 2025 11:40:44 -0300 Subject: [PATCH] feat: add openGraphTags to pages - Adds a new component that combines all meta tags for OpenGraph - Refactors GroupedTestStatus to receive a precalculated status count in order to reuse it for the TreeDetails description - Refactors TestDetails so that it receives the testId directly instead of as a component parameter - Fixes buildDetails general section title to consider null configs - Adds a function that returns the culprit code of an issue as a string so that it can be used in the description Closes #935 --- dashboard/public/kernelci-logo-card.png | Bin 0 -> 36233 bytes .../components/BuildDetails/BuildDetails.tsx | 46 +++++--- .../components/IssueDetails/IssueDetails.tsx | 48 ++++++--- .../OpenGraphTags/BuildDetailsOGTags.tsx | 53 +++++++++ .../OpenGraphTags/IssueDetailsOGTags.tsx | 51 +++++++++ .../OpenGraphTags/ListingOGTags.tsx | 58 ++++++++++ .../OpenGraphTags/OpenGraphTags.tsx | 29 +++++ .../OpenGraphTags/TestDetailsOGTags.tsx | 57 ++++++++++ .../TreeHardwareDetailsOGTags.tsx | 90 ++++++++++++++++ .../src/components/OpenGraphTags/index.tsx | 3 + dashboard/src/components/Status/Status.tsx | 25 +++-- .../components/TestDetails/TestDetails.tsx | 39 +++---- dashboard/src/lib/issue.ts | 29 +++++ dashboard/src/lib/test.ts | 20 ++++ dashboard/src/locales/messages/index.ts | 10 ++ dashboard/src/pages/Hardware/Hardware.tsx | 6 +- .../src/pages/IssueListing/IssueListing.tsx | 3 + .../src/pages/TreeDetails/TreeDetails.tsx | 96 +++++++++++------ dashboard/src/pages/Trees.tsx | 6 +- .../pages/hardwareDetails/HardwareDetails.tsx | 101 ++++++++++++------ dashboard/src/utils/status.ts | 2 +- 21 files changed, 635 insertions(+), 137 deletions(-) create mode 100644 dashboard/public/kernelci-logo-card.png create mode 100644 dashboard/src/components/OpenGraphTags/BuildDetailsOGTags.tsx create mode 100644 dashboard/src/components/OpenGraphTags/IssueDetailsOGTags.tsx create mode 100644 dashboard/src/components/OpenGraphTags/ListingOGTags.tsx create mode 100644 dashboard/src/components/OpenGraphTags/OpenGraphTags.tsx create mode 100644 dashboard/src/components/OpenGraphTags/TestDetailsOGTags.tsx create mode 100644 dashboard/src/components/OpenGraphTags/TreeHardwareDetailsOGTags.tsx create mode 100644 dashboard/src/components/OpenGraphTags/index.tsx create mode 100644 dashboard/src/lib/issue.ts create mode 100644 dashboard/src/lib/test.ts diff --git a/dashboard/public/kernelci-logo-card.png b/dashboard/public/kernelci-logo-card.png new file mode 100644 index 0000000000000000000000000000000000000000..4350b4af8a2c6820ca3825f8e95f79ab343c5d3e GIT binary patch literal 36233 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

2N zX|*=JER2P)99mcNJzFSgYHEUXr4H^^`zSerO8Ba zz4aJaMh)+K5w*y$(8RwmF9gptKl~7>sS^dLnA!e-n<{-tXu~VkML_?C(y>pq^FQo@ zg%7X%gAnxbV@_XC2W2AmQpvo{oXLvr$=Mqb87P%3zT$a)a5s8V&Lk2n zx&2POxVXhdKsx5N-ZMC+0+^R_mVwz*FnQP=r_ZQqv0t`q=>U4S&;9$ImD5NR0~t<~ zpNxMXXoMh;jP31oCt@Y|4j3*K59LS;upX1W@R^~X>g!8UkO=8aAq%66-=;vM9_$}! zrUPdRGbBFsxo88ut5eBZ-ga^7;z-CV&~0E*`vT-IUw#@fp`_#j>^F2xCGHj891?VW z=?@&Za}x~{plGH;7WfN>8((QHWOkaJU!@%*jZM~_4aQBmNl}1*0M*9A1ZxNU#?5q| zA{V06;wAev}#MvdFiPq{5L+6Y31tE$d)e14A=K<+}k!7&v}1j>QRMey_a-(#iKlP74Zh|m~Gg~*Aist9yMKQu)6*zo}tmV)$4qoMR$!`>8$#+A>E1rpp z!r=5Xr6sK@TW>8u31!s5o%m@c<+G^~_MD!56COOje#-*)Q<43H{lGLD5<0G6 z5OoOBq~DjfnzRqxSBi5@v zBF60YyRlUg%si8lYa%B(UNd3s(mZ-n-P$CLcPuox`Xtz%8&lL3D2X`ijIII*vft35 zYhjLWdd|M}cqfMm87a5LHw<}sP9SJt17=fNo} zmMLQ3HE!a7`7Ki|zwl=Za&uJ)KgcN|LhRQfi`3Wd5l%};XQWxvFj~H5%dK|U9Wc#7 zYDxx!j7lWq(g!k-$F|3AB3Tn?!P7nMzA&snND}+BBN!Nrl-VBO*5A=q#6fh)INH#| zzJP-vdVZ*r-%~^pIh%hMzPRJYtwkY1q+=rVgrh^JSBMhdOevdkH;S?~usgzVMz z`)ns^gg*+!sNt0xwiWQnPfz1#C#Q4x?6-N88XO(LZ-zFyP|1C2ZpH*nQ$xIdE?O2K z`Q_aciuj?tjbar;VYITdJMrs}r#oLsoK@1btIN7|y(2zQaPY5bFhNp9r;o@w0EFI_ zz;y87!TFt<35Xn>g_emWg-JJmq@$8_F?|f(2t?AL;9zBUC9V{@9tVe}Uyp7s zBg4ZXA*6-=y?d`~d^x9d{WJW!dC$vWyz#t1`3=pHwHHs4UPBUKoEf< zznP;ZB&!iDXe&-bbf%&>U>un`;QNJ&m)ap(UiFN)Ev`!Z%3BftH%uIdEnd0!^*{b2 zahR%jvfnTG`-#iqw#Dy}OU1AMu^(-jY9x}ufBZWBpU3fE`9^;%tpC_+`+pwS|NhZ` t$p-kpF5|yr1N`sD^{*Fdr1Guv>*=e`%$?MDLu`&e4CfjoPG7R^e*hCI@ZbOd literal 0 HcmV?d00001 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 ( - + +