-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
feat: add eol page #7990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add eol page #7990
Changes from 5 commits
02db91b
113334f
6bc1393
967676d
5cd1b33
d2b7826
b294b44
4204159
2cadb0c
55abd69
1a4a53a
51c5fb0
ea43096
d94d5ce
1dccbd5
21014bd
ff6ddec
9c68dd0
f8cab5e
2fbd30a
7babcc8
07befbb
594531f
c4abb0e
e471cb0
6b7bde7
2a748fd
e0f7c44
5ab2cbb
26f5b81
16f5992
ccbba0f
40df1ab
f424d53
7724afb
669d21d
04e4f5c
89e5c92
748e116
9fe21cc
4836bef
c147efc
847897b
d61bd30
1a522c8
dd10604
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,23 +5,19 @@ import type { FC } from 'react'; | |
import { use } from 'react'; | ||
|
||
import LinkWithArrow from '#site/components/LinkWithArrow'; | ||
import { ReleaseModalContext } from '#site/providers/releaseModalProvider'; | ||
import type { NodeRelease } from '#site/types'; | ||
import { ModalContext } from '#site/providers/modalProvider'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Context's should be imported from a hook. Like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And these should be explicitly defined as client-side hooks, so that if imported from server it fails. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the previous implementation, a "useReleaseModalContext" hook wasn't in use, this follows that principle |
||
|
||
type DetailsButtonProps = { | ||
versionData: NodeRelease; | ||
data: unknown; | ||
}; | ||
|
||
const DetailsButton: FC<DetailsButtonProps> = ({ versionData }) => { | ||
const DetailsButton: FC<DetailsButtonProps> = ({ data }) => { | ||
const t = useTranslations('components.downloadReleasesTable'); | ||
|
||
const { openModal } = use(ReleaseModalContext); | ||
const { openModal } = use(ModalContext); | ||
|
||
return ( | ||
<LinkWithArrow | ||
className="cursor-pointer" | ||
onClick={() => openModal(versionData)} | ||
> | ||
<LinkWithArrow className="cursor-pointer" onClick={() => openModal(data)}> | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{t('details')} | ||
</LinkWithArrow> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import AlertBox from '@node-core/ui-components/Common/AlertBox'; | ||
import { useTranslations } from 'next-intl'; | ||
|
||
import Link from '#site/components/Link'; | ||
|
||
const EOLAlert = () => { | ||
const t = useTranslations('components.endOfLife'); | ||
return ( | ||
<AlertBox level="warning"> | ||
{t('intro')}{' '} | ||
<Link href="/eol"> | ||
OpenJS Ecosystem Sustainability Program {t('partner')} HeroDevs | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</Link> | ||
</AlertBox> | ||
); | ||
}; | ||
|
||
export default EOLAlert; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { Modal, Title, Content } from '@node-core/ui-components/Common/Modal'; | ||
import classNames from 'classnames'; | ||
import { useTranslations } from 'next-intl'; | ||
import type { FC } from 'react'; | ||
|
||
import VulnerabilityChip from '#site/components/EOL/VulnerabilityChips/Chip'; | ||
import LinkWithArrow from '#site/components/LinkWithArrow'; | ||
import type { ModalProps } from '#site/providers/modalProvider'; | ||
import type { NodeRelease } from '#site/types'; | ||
import type { Vulnerability } from '#site/types/vulnerabilities'; | ||
|
||
import { SEVERITY_ORDER } from './VulnerabilityChips'; | ||
|
||
type EOLModalData = { | ||
release: NodeRelease; | ||
vulnerabilities: Array<Vulnerability>; | ||
}; | ||
|
||
type KnownVulnerability = Vulnerability & { | ||
severity: (typeof SEVERITY_ORDER)[number]; | ||
}; | ||
|
||
const VulnerabilitiesTable: FC<{ | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
vulnerabilities: Array<Vulnerability>; | ||
maxWidth?: string; | ||
}> = ({ vulnerabilities, maxWidth = 'max-w-2xs' }) => { | ||
const t = useTranslations('components.eolModal'); | ||
|
||
return ( | ||
<table className="w-full"> | ||
<thead> | ||
<tr> | ||
<th>{t('table.cves')}</th> | ||
<th>{t('table.severity')}</th> | ||
<th>{t('table.overview')}</th> | ||
<th>{t('table.details')}</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{vulnerabilities.map((vuln, i) => ( | ||
<tr key={i}> | ||
<td> | ||
{vuln.cve.length | ||
? vuln.cve.map(cveId => ( | ||
ovflowd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<div key={cveId}> | ||
<LinkWithArrow | ||
href={`https://www.cve.org/CVERecord?id=${cveId}`} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
{cveId} | ||
</LinkWithArrow> | ||
</div> | ||
)) | ||
: '-'} | ||
</td> | ||
<td> | ||
<VulnerabilityChip severity={vuln.severity} /> | ||
</td> | ||
<td className={classNames(maxWidth, 'truncate')}> | ||
{vuln.description || vuln.overview || '-'} | ||
</td> | ||
<td> | ||
{vuln.ref ? ( | ||
<LinkWithArrow | ||
href={vuln.ref} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
{t('blogLinkText')} | ||
</LinkWithArrow> | ||
) : ( | ||
'—' | ||
)} | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
}; | ||
|
||
const UnknownSeveritySection: FC<{ | ||
vulnerabilities: Array<Vulnerability>; | ||
ovflowd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
hasKnownVulns: boolean; | ||
}> = ({ vulnerabilities, hasKnownVulns }) => { | ||
const t = useTranslations('components.eolModal'); | ||
|
||
if (!vulnerabilities.length) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<details open={!hasKnownVulns}> | ||
<summary className="cursor-pointer font-semibold"> | ||
{t('showUnknownSeverities')} ({vulnerabilities.length}) | ||
</summary> | ||
<div className="mt-4"> | ||
<VulnerabilitiesTable | ||
vulnerabilities={vulnerabilities} | ||
maxWidth={'max-w-3xs'} | ||
/> | ||
</div> | ||
</details> | ||
); | ||
}; | ||
|
||
const EOLModal: FC<ModalProps> = ({ open, closeModal, data }) => { | ||
const { release, vulnerabilities } = data as EOLModalData; | ||
const t = useTranslations('components.eolModal'); | ||
|
||
const modalHeading = t(release.codename ? 'title' : 'titleWithoutCodename', { | ||
version: release.major, | ||
codename: release.codename ?? '', | ||
}); | ||
|
||
const [knownVulns, unknownVulns] = vulnerabilities.reduce( | ||
ovflowd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(acc, vuln) => { | ||
acc[vuln.severity === 'unknown' ? 1 : 0].push(vuln as KnownVulnerability); | ||
return acc; | ||
}, | ||
[[], []] as [Array<KnownVulnerability>, Array<Vulnerability>] | ||
); | ||
|
||
knownVulns.sort( | ||
ovflowd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(a, b) => | ||
SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity) | ||
); | ||
|
||
const hasKnownVulns = knownVulns.length > 0; | ||
const hasAnyVulns = hasKnownVulns || unknownVulns.length > 0; | ||
|
||
return ( | ||
<Modal open={open} onOpenChange={closeModal}> | ||
<Title>{modalHeading}</Title> | ||
<Content> | ||
{vulnerabilities.length > 0 && ( | ||
<p className="m-1"> | ||
{t('vulnerabilitiesMessage', { count: vulnerabilities.length })} | ||
</p> | ||
)} | ||
|
||
{hasKnownVulns && <VulnerabilitiesTable vulnerabilities={knownVulns} />} | ||
|
||
<UnknownSeveritySection | ||
vulnerabilities={unknownVulns} | ||
hasKnownVulns={hasKnownVulns} | ||
/> | ||
|
||
{!hasAnyVulns && <p className="m-1">{t('noVulnerabilitiesMessage')}</p>} | ||
</Content> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default EOLModal; | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { getTranslations } from 'next-intl/server'; | ||
import type { FC } from 'react'; | ||
|
||
import FormattedTime from '#site/components/Common/FormattedTime'; | ||
import DetailsButton from '#site/components/Downloads/DownloadReleasesTable/DetailsButton'; | ||
import provideReleaseData from '#site/next-data/providers/releaseData'; | ||
import provideVulnerabilities from '#site/next-data/providers/vulnerabilities'; | ||
|
||
import VulnerabilityChips from './VulnerabilityChips'; | ||
|
||
const EOLTable: FC = async () => { | ||
const releaseData = provideReleaseData(); | ||
const vulnerabilities = await provideVulnerabilities(); | ||
const EOLReleases = releaseData.filter( | ||
ovflowd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
release => release.status === 'End-of-life' | ||
); | ||
|
||
const t = await getTranslations(); | ||
|
||
return ( | ||
<table id="tbVulnerabilities" className="download-table full-width"> | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<thead> | ||
<tr> | ||
<th> | ||
{t('components.eolTable.version')} ( | ||
{t('components.eolTable.codename')}) | ||
</th> | ||
<th>{t('components.eolTable.lastUpdated')}</th> | ||
<th>{t('components.eolTable.vulnerabilities')}</th> | ||
<th>{t('components.eolTable.details')}</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{EOLReleases.map(release => ( | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<tr key={release.major}> | ||
<td data-label="Version"> | ||
v{release.major} {release.codename ? `(${release.codename})` : ''} | ||
</td> | ||
<td data-label="Date"> | ||
<FormattedTime date={release.releaseDate} /> | ||
</td> | ||
<td> | ||
<VulnerabilityChips | ||
vulnerabilities={vulnerabilities[release.major]} | ||
ovflowd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/> | ||
</td> | ||
<td className="download-table-last"> | ||
<DetailsButton | ||
data={{ | ||
release: release, | ||
vulnerabilities: vulnerabilities[release.major], | ||
}} | ||
/> | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
}; | ||
|
||
export default EOLTable; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
@reference "../../../../styles/index.css"; | ||
|
||
.chipCount { | ||
@apply mr-1 | ||
rounded-sm | ||
bg-gray-800/20 | ||
px-1.5; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Badge from '@node-core/ui-components/Common/Badge'; | ||
import { useTranslations } from 'next-intl'; | ||
import type { FC } from 'react'; | ||
|
||
import styles from './index.module.css'; | ||
|
||
export const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'] as const; | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const SEVERITY_KIND_MAP = { | ||
bmuenzenmeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
unknown: 'neutral', | ||
low: 'default', | ||
medium: 'info', | ||
high: 'warning', | ||
critical: 'error', | ||
} as const; | ||
|
||
type VulnerabilityChipProps = { | ||
severity: keyof typeof SEVERITY_KIND_MAP; | ||
count?: number; | ||
}; | ||
|
||
const VulnerabilityChip: FC<VulnerabilityChipProps> = ({ | ||
severity, | ||
count = 0, | ||
}) => { | ||
const t = useTranslations('components.endOfLife'); | ||
|
||
return ( | ||
<Badge size="small" kind={SEVERITY_KIND_MAP[severity]} className="mr-0.5"> | ||
{count > 0 ? <span className={styles.chipCount}>{count}</span> : null} | ||
{t(`severity.${severity}`)} | ||
</Badge> | ||
); | ||
}; | ||
|
||
export default VulnerabilityChip; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this approach of modal metadata to be injected at layout-level, it implies you cannnot have different sort of modals per page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even less making it part of the frontmatter, which is even worse IMO.