Skip to content

Commit 02db91b

Browse files
feat: add eol page
Co-Authored-By: Aviv Keller <me@aviv.sh>
1 parent 439f978 commit 02db91b

File tree

26 files changed

+575
-84
lines changed

26 files changed

+575
-84
lines changed

apps/site/app/[locale]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ const getPage: FC<DynamicParams> = async props => {
150150
// within a server-side context
151151
return (
152152
<MatterProvider {...sharedContext}>
153-
<WithLayout layout={frontmatter.layout}>{content}</WithLayout>
153+
<WithLayout layout={frontmatter.layout} modal={frontmatter.modal}>
154+
{content}
155+
</WithLayout>
154156
</MatterProvider>
155157
);
156158
}

apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,19 @@ import type { FC } from 'react';
55
import { use } from 'react';
66

77
import LinkWithArrow from '#site/components/LinkWithArrow';
8-
import { ReleaseModalContext } from '#site/providers/releaseModalProvider';
9-
import type { NodeRelease } from '#site/types';
8+
import { ModalContext } from '#site/providers/modalProvider';
109

1110
type DetailsButtonProps = {
12-
versionData: NodeRelease;
11+
data: unknown;
1312
};
1413

15-
const DetailsButton: FC<DetailsButtonProps> = ({ versionData }) => {
14+
const DetailsButton: FC<DetailsButtonProps> = ({ data }) => {
1615
const t = useTranslations('components.downloadReleasesTable');
1716

18-
const { openModal } = use(ReleaseModalContext);
17+
const { openModal } = use(ModalContext);
1918

2019
return (
21-
<LinkWithArrow
22-
className="cursor-pointer"
23-
onClick={() => openModal(versionData)}
24-
>
20+
<LinkWithArrow className="cursor-pointer" onClick={() => openModal(data)}>
2521
{t('details')}
2622
</LinkWithArrow>
2723
);

apps/site/components/Downloads/DownloadReleasesTable/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const DownloadReleasesTable: FC = () => {
4848
</Badge>
4949
</td>
5050
<td className="download-table-last">
51-
<DetailsButton versionData={release} />
51+
<DetailsButton data={release} />
5252
</td>
5353
</tr>
5454
))}

apps/site/components/Downloads/Release/ReleaseCodeBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const ReleaseCodeBox: FC = () => {
122122
size="small"
123123
>
124124
{t.rich('layouts.download.codeBox.unsupportedVersionWarning', {
125-
link: text => <Link href="/about/previous-releases/">{text}</Link>,
125+
link: text => <Link href="/eol">{text}</Link>,
126126
})}
127127
</AlertBox>
128128
)}

apps/site/components/Downloads/ReleaseModal/index.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,11 @@ import type { FC } from 'react';
66
import { MinorReleasesTable } from '#site/components/Downloads/MinorReleasesTable';
77
import { ReleaseOverview } from '#site/components/Downloads/ReleaseOverview';
88
import Link from '#site/components/Link';
9+
import type { ModalProps } from '#site/providers/modalProvider';
910
import type { NodeRelease } from '#site/types';
1011

11-
type ReleaseModalProps = {
12-
isOpen: boolean;
13-
closeModal: () => void;
14-
release: NodeRelease;
15-
};
16-
17-
const ReleaseModal: FC<ReleaseModalProps> = ({
18-
isOpen,
19-
closeModal,
20-
release,
21-
}) => {
12+
const ReleaseModal: FC<ModalProps> = ({ open, closeModal, data }) => {
13+
const release = data as NodeRelease;
2214
const t = useTranslations();
2315

2416
const modalHeadingKey = release.codename
@@ -31,7 +23,7 @@ const ReleaseModal: FC<ReleaseModalProps> = ({
3123
});
3224

3325
return (
34-
<Modal open={isOpen} onOpenChange={closeModal}>
26+
<Modal open={open} onOpenChange={closeModal}>
3527
{release.status === 'End-of-life' && (
3628
<div className="mb-4">
3729
<AlertBox
@@ -41,10 +33,7 @@ const ReleaseModal: FC<ReleaseModalProps> = ({
4133
>
4234
{t.rich('components.releaseModal.unsupportedVersionWarning', {
4335
link: text => (
44-
<Link
45-
onClick={closeModal}
46-
href="/about/previous-releases#release-schedule"
47-
>
36+
<Link onClick={closeModal} href="/eol">
4837
{text}
4938
</Link>
5039
),

apps/site/components/EOL/Alert.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import AlertBox from '@node-core/ui-components/Common/AlertBox';
2+
import { useTranslations } from 'next-intl';
3+
4+
import Link from '#site/components/Link';
5+
6+
const EOLAlert = () => {
7+
const t = useTranslations('components.endOfLife');
8+
return (
9+
<AlertBox level="warning">
10+
{t('intro')}{' '}
11+
<Link href="/eol">
12+
OpenJS Ecosystem Sustainability Program partner HeroDevs
13+
</Link>
14+
</AlertBox>
15+
);
16+
};
17+
18+
export default EOLAlert;

apps/site/components/EOL/Modal.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Modal, Title, Content } from '@node-core/ui-components/Common/Modal';
2+
import classNames from 'classnames';
3+
import { useTranslations } from 'next-intl';
4+
import type { FC } from 'react';
5+
6+
import VulnerabilityChip from '#site/components/EOL/VulnerabilityChips/Chip';
7+
import LinkWithArrow from '#site/components/LinkWithArrow';
8+
import type { ModalProps } from '#site/providers/modalProvider';
9+
import type { NodeRelease } from '#site/types';
10+
import type { Vulnerability } from '#site/types/vulnerabilities';
11+
12+
import { SEVERITY_ORDER } from './VulnerabilityChips';
13+
14+
type EOLModalData = {
15+
release: NodeRelease;
16+
vulnerabilities: Array<Vulnerability>;
17+
};
18+
19+
type KnownVulnerability = Vulnerability & {
20+
severity: (typeof SEVERITY_ORDER)[number];
21+
};
22+
23+
const VulnerabilitiesTable: FC<{
24+
vulnerabilities: Array<Vulnerability>;
25+
maxWidth?: string;
26+
}> = ({ vulnerabilities, maxWidth = 'max-w-2xs' }) => {
27+
const t = useTranslations('components.eolModal');
28+
29+
return (
30+
<table className="w-full">
31+
<thead>
32+
<tr>
33+
<th>{t('table.cves')}</th>
34+
<th>{t('table.severity')}</th>
35+
<th>{t('table.overview')}</th>
36+
<th>{t('table.details')}</th>
37+
</tr>
38+
</thead>
39+
<tbody>
40+
{vulnerabilities.map((vuln, i) => (
41+
<tr key={i}>
42+
<td>
43+
{vuln.cve.length
44+
? vuln.cve.map(cveId => (
45+
<div key={cveId}>
46+
<LinkWithArrow
47+
href={`https://cve.mitre.org/cgi-bin/cvename.cgi?name=${cveId}`}
48+
target="_blank"
49+
rel="noopener noreferrer"
50+
>
51+
{cveId}
52+
</LinkWithArrow>
53+
</div>
54+
))
55+
: '-'}
56+
</td>
57+
<td>
58+
<VulnerabilityChip severity={vuln.severity} />
59+
</td>
60+
<td className={classNames(maxWidth, 'truncate')}>
61+
{vuln.description || vuln.overview || '-'}
62+
</td>
63+
<td>
64+
{vuln.ref ? (
65+
<LinkWithArrow
66+
href={vuln.ref}
67+
target="_blank"
68+
rel="noopener noreferrer"
69+
>
70+
{t('blogLinkText')}
71+
</LinkWithArrow>
72+
) : (
73+
'—'
74+
)}
75+
</td>
76+
</tr>
77+
))}
78+
</tbody>
79+
</table>
80+
);
81+
};
82+
83+
const UnknownSeveritySection: FC<{
84+
vulnerabilities: Array<Vulnerability>;
85+
hasKnownVulns: boolean;
86+
}> = ({ vulnerabilities, hasKnownVulns }) => {
87+
const t = useTranslations('components.eolModal');
88+
89+
if (!vulnerabilities.length) {
90+
return null;
91+
}
92+
93+
return (
94+
<details open={!hasKnownVulns}>
95+
<summary className="cursor-pointer font-semibold">
96+
{t('showUnknownSeverities')} ({vulnerabilities.length})
97+
</summary>
98+
<div className="mt-4">
99+
<VulnerabilitiesTable
100+
vulnerabilities={vulnerabilities}
101+
maxWidth={'max-w-3xs'}
102+
/>
103+
</div>
104+
</details>
105+
);
106+
};
107+
108+
const EOLModal: FC<ModalProps> = ({ open, closeModal, data }) => {
109+
const { release, vulnerabilities } = data as EOLModalData;
110+
const t = useTranslations('components.eolModal');
111+
112+
const modalHeading = t(release.codename ? 'title' : 'titleWithoutCodename', {
113+
version: release.major,
114+
codename: release.codename ?? '',
115+
});
116+
117+
const [knownVulns, unknownVulns] = vulnerabilities.reduce(
118+
(acc, vuln) => {
119+
acc[vuln.severity === 'unknown' ? 1 : 0].push(vuln as KnownVulnerability);
120+
return acc;
121+
},
122+
[[], []] as [Array<KnownVulnerability>, Array<Vulnerability>]
123+
);
124+
125+
knownVulns.sort(
126+
(a, b) =>
127+
SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity)
128+
);
129+
130+
const hasKnownVulns = knownVulns.length > 0;
131+
const hasAnyVulns = hasKnownVulns || unknownVulns.length > 0;
132+
133+
return (
134+
<Modal open={open} onOpenChange={closeModal}>
135+
<Title>{modalHeading}</Title>
136+
<Content>
137+
{vulnerabilities.length > 0 && (
138+
<p className="m-1">
139+
{t('vulnerabilitiesMessage', { count: vulnerabilities.length })}
140+
</p>
141+
)}
142+
143+
{hasKnownVulns && <VulnerabilitiesTable vulnerabilities={knownVulns} />}
144+
145+
<UnknownSeveritySection
146+
vulnerabilities={unknownVulns}
147+
hasKnownVulns={hasKnownVulns}
148+
/>
149+
150+
{!hasAnyVulns && <p className="m-1">{t('noVulnerabilitiesMessage')}</p>}
151+
</Content>
152+
</Modal>
153+
);
154+
};
155+
156+
export default EOLModal;

apps/site/components/EOL/Table.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { getTranslations } from 'next-intl/server';
2+
import type { FC } from 'react';
3+
4+
import FormattedTime from '#site/components/Common/FormattedTime';
5+
import DetailsButton from '#site/components/Downloads/DownloadReleasesTable/DetailsButton';
6+
import provideReleaseData from '#site/next-data/providers/releaseData';
7+
import provideVulnerabilities from '#site/next-data/providers/vulnerabilities';
8+
9+
import VulnerabilityChips from './VulnerabilityChips';
10+
11+
const EOLTable: FC = async () => {
12+
const releaseData = provideReleaseData();
13+
const vulnerabilities = await provideVulnerabilities();
14+
const EOLReleases = releaseData.filter(
15+
release => release.status === 'End-of-life'
16+
);
17+
18+
const t = await getTranslations();
19+
20+
return (
21+
<table id="tbVulnerabilities" className="download-table full-width">
22+
<thead>
23+
<tr>
24+
{/* TODO @bmuenzenmeyer change these to new i18n keys */}
25+
<th>
26+
{t('components.downloadReleasesTable.version')} (
27+
{t('components.downloadReleasesTable.codename')})
28+
</th>
29+
<th>{t('components.downloadReleasesTable.lastUpdated')}</th>
30+
<th>Vulnerabilities</th>
31+
<th>Details</th>
32+
</tr>
33+
</thead>
34+
<tbody>
35+
{EOLReleases.map(release => (
36+
<tr key={release.major}>
37+
<td data-label="Version">
38+
v{release.major} {release.codename ? `(${release.codename})` : ''}
39+
</td>
40+
<td data-label="Date">
41+
<FormattedTime date={release.releaseDate} />
42+
</td>
43+
<td>
44+
<VulnerabilityChips
45+
vulnerabilities={vulnerabilities[release.major]}
46+
/>
47+
</td>
48+
<td className="download-table-last">
49+
<DetailsButton
50+
data={{
51+
release: release,
52+
vulnerabilities: vulnerabilities[release.major],
53+
}}
54+
/>
55+
</td>
56+
</tr>
57+
))}
58+
</tbody>
59+
</table>
60+
);
61+
};
62+
63+
export default EOLTable;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@reference "../../../../styles/index.css";
2+
3+
.chipCount {
4+
@apply mr-1
5+
rounded-sm
6+
bg-gray-800/20
7+
px-1.5;
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Badge from '@node-core/ui-components/Common/Badge';
2+
import { useTranslations } from 'next-intl';
3+
import type { FC } from 'react';
4+
5+
import styles from './index.module.css';
6+
7+
export const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'] as const;
8+
9+
const SEVERITY_KIND_MAP = {
10+
unknown: 'neutral',
11+
low: 'default',
12+
medium: 'info',
13+
high: 'warning',
14+
critical: 'error',
15+
} as const;
16+
17+
type VulnerabilityChipProps = {
18+
severity: keyof typeof SEVERITY_KIND_MAP;
19+
count?: number;
20+
};
21+
22+
const VulnerabilityChip: FC<VulnerabilityChipProps> = ({
23+
severity,
24+
count = 0,
25+
}) => {
26+
const t = useTranslations('components.endOfLife');
27+
28+
return (
29+
<Badge size="small" kind={SEVERITY_KIND_MAP[severity]} className="mr-0.5">
30+
{count > 0 ? <span className={styles.chipCount}>{count}</span> : null}
31+
{t(`severity.${severity}`)}
32+
</Badge>
33+
);
34+
};
35+
36+
export default VulnerabilityChip;

0 commit comments

Comments
 (0)