From d506f6c3f2d34fee2c816162fe32933e4fc9c589 Mon Sep 17 00:00:00 2001 From: Haoming Meng Date: Thu, 23 May 2024 10:34:14 -0500 Subject: [PATCH 1/2] Download card improvement --- components/DownloadsComponent.tsx | 123 ++++++++++++++++--------- components/Filters.tsx | 40 ++------ components/ReleasesTable.tsx | 33 ++++--- public/static/releases-table-data.json | 10 +- utils/fetchReleases.tsx | 71 ++++++++------ utils/types.ts | 67 ++++++++++---- utils/utils.ts | 8 ++ 7 files changed, 212 insertions(+), 140 deletions(-) create mode 100644 utils/utils.ts diff --git a/components/DownloadsComponent.tsx b/components/DownloadsComponent.tsx index 6041649..6dc6d01 100644 --- a/components/DownloadsComponent.tsx +++ b/components/DownloadsComponent.tsx @@ -1,27 +1,30 @@ "use client" import React, { useState, useEffect, useMemo } from 'react'; -import { Box, CircularProgress } from '@mui/material'; +import { Box, CircularProgress, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'; import fetchFilteredReleases from "../utils/fetchReleases"; -import { FilteredRelease } from '../utils/types'; +import { FilteredRelease, ArchEnums, OSEnums, SemverRegex } from '../utils/types'; import {OperatingSystems, Architectures} from './Filters'; import ReleasesTable from './ReleasesTable'; import data from '../public/static/releases-table-data.json'; import { DarkLightContainer } from '@/utils/darkLightContainer'; import { useTheme } from '@mui/material/styles'; +import { parseEnum } from '@/utils/utils'; -const architectures = { - "PowerPC": ["ppc64el", "ppc64le"], - "ARM64": ["aarch64", "arm64"], - "AMD64": ["amd64", "x86_64"] -}; +interface optionMatrix { + arch: ArchEnums | "" + os: OSEnums | "" + version: string +} const DownloadsComponent: React.FC = () => { const [originalData, setOriginalData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [selectedOptions, setSelectedOptions] = React.useState({ - arch: 'AMD64', - os: 'linux', + const [versions, setVersions] = useState([]) + const [selectedOptions, setSelectedOptions] = useState({ + arch: ArchEnums.X86_64, + os: OSEnums.Linux, + version: "" }); const theme = useTheme(); @@ -33,7 +36,7 @@ const DownloadsComponent: React.FC = () => { if (newArch !== selectedOptions.arch) { setSelectedOptions(prevOptions => ({ ...prevOptions, - arch: newArch || '' + arch: newArch as ArchEnums || '' })); } }; @@ -45,7 +48,7 @@ const DownloadsComponent: React.FC = () => { if (newOs !== selectedOptions.os) { setSelectedOptions(prevOptions => ({ ...prevOptions, - os: newOs || '' + os: newOs as OSEnums || '' })); } }; @@ -56,7 +59,8 @@ const DownloadsComponent: React.FC = () => { try { const releases = await fetchFilteredReleases(); // This function should return an array of FilteredRelease setOriginalData(releases); - console.log(releases); + setSelectedOptions((prev) => ({...prev, version: prev.version ? prev.version : releases[0].version})) + setVersions(releases.map(release => release.version).filter(version => version >= "v7.6.5")) } catch (e) { setError('Failed to fetch release assets'); console.error(e); @@ -64,36 +68,63 @@ const DownloadsComponent: React.FC = () => { setLoading(false); } }; + + const params = new URLSearchParams(window?.location.search) + const qVersion = params.get("version") + const qArch = parseEnum(params.get("arch"), ArchEnums) + const qOS = parseEnum(params.get("os"), OSEnums) + const queryMatrix: optionMatrix = { + arch: qArch || '', + os: qOS || '', + version: SemverRegex.test(qVersion) ? qVersion : "" + } + setSelectedOptions((prev) => ( + { + arch: queryMatrix.arch ? queryMatrix.arch : prev.arch, + os: queryMatrix.os ? queryMatrix.os : prev.os, + version: queryMatrix.version ? queryMatrix.version : prev.version, + } + )) + fetchAssets(); }, []); + const filteredData = useMemo(() => { - const architectureIdentifiers = selectedOptions.arch ? (architectures[selectedOptions.arch] || []) : []; + console.log("update with", selectedOptions) + const selectedArch = selectedOptions.arch; + const filteredByVersion = structuredClone(originalData.filter((release) => release.version == selectedOptions.version)[0]) + if (!filteredByVersion) { + return undefined + } + // Now, filter assets within those releases based on the selected OS and Arch - const releasesWithFilteredAssets = originalData.map(release => { - const filteredAssets = release.assets.filter(asset => { - const osMatch = !selectedOptions.os || asset.operatingSystem.toLowerCase().includes(selectedOptions.os.toLowerCase()); - const archMatch = !selectedOptions.arch || architectureIdentifiers.some(archIdentifier => asset.architecture.includes(archIdentifier)); + const filteredAssets = filteredByVersion.assets.filter(asset => { + const osMatch = !selectedOptions.os || asset.osInternal.toLowerCase().includes(selectedOptions.os.toLowerCase()); + const archMatch = !selectedArch || asset.architecture === selectedArch; return osMatch && archMatch; }) .sort((a, b) => { - // Sort by file extension - const extA = a.name.split('.').pop(); - const extB = b.name.split('.').pop(); - if (extA < extB) return -1; - if (extA > extB) return 1; - - // If extensions are the same, sort by name - return a.name.localeCompare(b.name); + // Sort by OS + const byOS = a.osDisplayed.localeCompare(b.osDisplayed) + if (byOS === 0) { + if (a.specialPackage && b.specialPackage) { + return a.name.localeCompare(b.name) + } else if (a.specialPackage && !b.specialPackage) { + return 1 + } else { + return -1 + } + } else { + return byOS + } }); - - // Return the release with the filtered assets - return { ...release, assets: filteredAssets }; - }).filter(release => release.assets.length > 0); // Keep only releases with matching assets - - return releasesWithFilteredAssets; - }, [selectedOptions.arch, selectedOptions.os, originalData]); + + filteredByVersion.assets = filteredAssets + return filteredByVersion; + + }, [selectedOptions, originalData]); const renderContent = () => { @@ -110,18 +141,26 @@ const DownloadsComponent: React.FC = () => { alignItems: 'center', justifyContent: 'center', width: '100%', - margin: theme.spacing(1), [theme.breakpoints.down('sm')]: { flexDirection: 'column', }, }}> - - + + Version + + + + - {filteredData.map(release => ( - - )) - } + {filteredData && } ); } @@ -135,9 +174,7 @@ const DownloadsComponent: React.FC = () => { alignItems: 'center', margin: '1em auto', overflow: 'auto', - [theme.breakpoints.down('md')]: { - padding: theme.spacing(2), - }, + padding: theme.spacing(1), }}> {renderContent()} diff --git a/components/Filters.tsx b/components/Filters.tsx index 22da96d..66ccdd2 100644 --- a/components/Filters.tsx +++ b/components/Filters.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; -import { compatibilityRules, ArchitecturesProps, OperatingSystemsProps, VersionProps } from '../utils/types'; +import { compatibilityRules, ArchitecturesProps, OperatingSystemsProps } from '../utils/types'; -export const Architectures:React.FC = ({handle, defaultArch, archData, defaultOs}) => { +export const Architectures:React.FC = ({handle, defaultArch, archs, defaultOs}) => { const isDisabled = (arch: string) => { // If an OS is selected, check if the current arch is compatible @@ -12,10 +12,6 @@ export const Architectures:React.FC = ({handle, defaultArch, return false; }; - const architectureOptions = Object.entries(archData).map(([key, value]) => ({ - label: key, - value: value.join(', ') // Joining all chipset identifiers for a given architecture - })); return ( @@ -29,13 +25,13 @@ export const Architectures:React.FC = ({handle, defaultArch, onChange={handle} size="small" > - {architectureOptions.map((option) => ( + {archs.map((option) => ( - {option.label} + {option} ))} @@ -78,25 +74,3 @@ export const OperatingSystems:React.FC = ({handle, defaul ) } -export const Version:React.FC = ({handle, defaultVersion, data}) => { - return ( - - - Versions - - - {data.map((version) => ( - {version} - ))} - - - - ) -} diff --git a/components/ReleasesTable.tsx b/components/ReleasesTable.tsx index 987fdfc..6cd5d08 100644 --- a/components/ReleasesTable.tsx +++ b/components/ReleasesTable.tsx @@ -1,33 +1,44 @@ import React from 'react'; import { - Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Link, Tooltip} + Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Link, Tooltip, + Chip} from '@mui/material'; import InfoIcon from '@mui/icons-material/Info'; import { ReleasesTableProps } from '../utils/types'; -const ReleasesTable: React.FC = ({ release , data }) => { +const OSDFNote = "This package is compatible with Open Science Data Federation (OSDF). Download this package if you plan to use it in OSDF." +const ServerNote = "This package includes Pelican origin/cache server dependencies. Download this package if you want to serve a Pelican origin or cache server." + +const ReleasesTable: React.FC = ({ release , rowNames }) => { return( - +
- - {data.map((tableRows) => ( - {tableRows} - ))} - + + {rowNames.map((rowName) => ( + {rowName} + ))} + {release.assets.map((asset, index) => ( {release.version} {asset.architecture} + {asset.osDisplayed} {asset.name} - - - + + + { + asset.specialPackage && ( + + + + ) + } ))} diff --git a/public/static/releases-table-data.json b/public/static/releases-table-data.json index aa7e218..9f7e7e9 100644 --- a/public/static/releases-table-data.json +++ b/public/static/releases-table-data.json @@ -1,13 +1,9 @@ { - "operating_systems": [ - "linux", - "darwin", - "windows" - ], - "table_rows": [ "Version", "Architecture", - "File" + "OS", + "File", + "Note" ] } \ No newline at end of file diff --git a/utils/fetchReleases.tsx b/utils/fetchReleases.tsx index e6b9d42..948b9fc 100644 --- a/utils/fetchReleases.tsx +++ b/utils/fetchReleases.tsx @@ -1,4 +1,27 @@ -import { Release, FilteredRelease, packageType, operatingSystems, architectures } from './types'; +import { Release, FilteredRelease, packageDisplayedOS, OSEnums, archMapping } from './types'; + +function getDisplayedOS(filename: string) { + for (const rule of packageDisplayedOS) { + const regex = new RegExp(rule.regex); + if (regex.test(filename)) { + return rule.os; + } + } + return "Unknown"; +} + +function getArchitecture(filename: string) { + for (const [arch, patterns] of Object.entries(archMapping)) { + for (const pattern of patterns) { + const regex = new RegExp(pattern, 'i'); // 'i' flag makes the regex case-insensitive + if (regex.test(filename)) { + return arch; + } + } + } + return "Unknown"; +} + async function fetchFilteredReleases(): Promise { const response = await fetch('https://api.github.com/repos/PelicanPlatform/pelican/releases'); @@ -7,8 +30,6 @@ async function fetchFilteredReleases(): Promise { // Sort releases by version using semver sort (consider using a library for robust sorting) const sortedReleases = releases.sort((a, b) => b.tag_name.localeCompare(a.tag_name, undefined, {numeric: true, sensitivity: 'base'})); - // Extract major versions and find the latest minor for each - let majorVersions = new Set(); let filteredReleases: FilteredRelease[] = []; for (const release of sortedReleases) { @@ -17,33 +38,23 @@ async function fetchFilteredReleases(): Promise { continue; } - const [major, minor] = release.tag_name.replace('v', '').split('.').map(Number); - const majorVersion = `${major}.${minor}`; - - if (majorVersions.size < 1 && !majorVersions.has(majorVersion)) { - majorVersions.add(majorVersion); - - filteredReleases.push({ - version: release.tag_name, - prerelease: release.prerelease, - assets: release.assets.map(asset => { - const packageInfo = asset.name.includes('osdf') ? - 'osdf' : - Object.keys(packageType).find(type => asset.name.endsWith(type)) || undefined; - - return { - name: asset.name, - downloadUrl: asset.browser_download_url, - id: asset.id, - assetVersion: release.tag_name, - operatingSystem: asset.name.includes('checksums.txt') ? '' : operatingSystems.find(os => asset.name.includes(os)) || 'Linux', - architecture: asset.name.includes('checksums.txt') ? '' : architectures.find(arch => asset.name.includes(arch)) || 'unknown', - packageInfo: packageInfo, - packageDescription: packageInfo ? packageType[packageInfo] : undefined, - }; - }) - }); - } + filteredReleases.push({ + version: release.tag_name, + prerelease: release.prerelease, + assets: release.assets.map(asset => { + return { + name: asset.name, + downloadUrl: asset.browser_download_url, + id: asset.id, + assetVersion: release.tag_name, + osInternal: asset.name.includes('checksums.txt') ? '' : asset.name.includes('Darwin') ? 'macOS' : Object.values(OSEnums).find(os => asset.name.includes(os)) || 'Linux', + osDisplayed: getDisplayedOS(asset.name), + architecture: getArchitecture(asset.name), + specialPackage: asset.name.includes('-server-') ? "Server" : asset.name.includes('-osdf-') ? "OSDF" : "" + }; + }) + }); + } return filteredReleases; diff --git a/utils/types.ts b/utils/types.ts index 48ca675..310a91c 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -8,7 +8,7 @@ export interface ArchitecturesProps { handle: (event: React.MouseEvent, newAlignment: string) => void; defaultArch: string; defaultOs: string; - archData: { [key: string]: string[] }; + archs: string[]; } export interface OperatingSystemsProps { @@ -42,14 +42,16 @@ name: string; downloadUrl: string; id: number; assetVersion: string; // Version of the release this asset belongs to -operatingSystem: string; +specialPackage: "OSDF" | "Server" | ""; // Special package for OSDF/Pelican server +osInternal: string; +osDisplayed: string; architecture: string; packageInfo?: string; packageDescription?: string; } export interface ReleasesTableProps { - data: Array; + rowNames: Array; release: FilteredRelease; } @@ -78,24 +80,57 @@ export interface PreProps { children: React.ReactNode; } -export const operatingSystems = ['Darwin', 'Linux', 'Windows']; +export const SemverRegex = /^v(\d+)\.(\d+)\.(\d+)$/; + +export enum OSEnums { + Linux = "Linux", + macOS = "macOS", + Windows = "Windows" +} -export const architectures = ['arm64', 'amd64', 'ppc64le', 'ppc64el', 'x86_64', 'aarch64']; +export enum ArchEnums { + X86_64 = "X86_64", + ARM64 = "ARM64", + PowerPC = "PowerPC" +} -export const packageType = { - "rpm": "You want to install a .rpm package if you are using a Red Hat-based Linux distribution system such as: Red Hat Enterprise Linux, CentOS, Fedora, or openSUSE." - , "apk": "You want to install a .apk package if you are using an Alpine Linux system." - , "deb": "You want to install a .deb package if you are using a Linux distribution system such as: Debian, Ubuntu, or something similar." - , "zip": "If you want a more manual setup, you can download the .zip files and extract the binary where you need it." - , "tar.gz": "If you want a more manual setup, you can download the .tar.gz files and extract the binary where you need it." - , "osdf": "Install this package if you want more convenient access to OSDF, including the stashcp program and the HTCondor file transfer plugin for osdf:// URLs" - , "txt": "Download this file to verify the correctness of your other downloads." +export const archMapping = { + [ArchEnums.X86_64]: ["amd64", "x86_64"], + [ArchEnums.ARM64]: ["aarch64", "arm64"], + [ArchEnums.PowerPC]: ["ppc64el", "ppc64le"], }; export const compatibilityRules = { - "darwin": ["ARM64", "AMD64"], - "windows": ["AMD64"], - "linux": ["PowerPC", "ARM64", "AMD64"] + [OSEnums.macOS]: [ArchEnums.ARM64, ArchEnums.X86_64], + [OSEnums.Windows]: [ArchEnums.X86_64], + [OSEnums.Linux]: [ArchEnums.PowerPC, ArchEnums.ARM64, ArchEnums.X86_64] }; +export const packageDisplayedOS = [ + { + regex: ".*\\.apk$", + os: "Alpine" + }, + { + regex: ".*\\.deb$", + os: "Ubuntu" + }, + { + regex: ".*\\.rpm$", + os: "RHEL" + }, + { + regex: ".*_Darwin_.*", + os: "macOS" + }, + { + regex: ".*_Windows_.*", + os: "Windows" + }, + { + regex: ".*_Linux_.*\\.tar\\.gz$", + os: "Linux" + } +] + export const parameterGroups = ["origin", "registry", "director", "client", "cache"]; \ No newline at end of file diff --git a/utils/utils.ts b/utils/utils.ts new file mode 100644 index 0000000..225478c --- /dev/null +++ b/utils/utils.ts @@ -0,0 +1,8 @@ +export function parseEnum(value: any, enumObj: T): T[keyof T] | undefined { + if (typeof value === 'string') { + if (Object.values(enumObj).includes(value)) { + return value as T[keyof T]; + } + } + return undefined; +} \ No newline at end of file From 023bf1ee1b2a6ce6992d0e7e09c21bbc77098538 Mon Sep 17 00:00:00 2001 From: Haoming Meng Date: Thu, 23 May 2024 10:45:55 -0500 Subject: [PATCH 2/2] Code clean up and organize --- components/DownloadsComponent.tsx | 16 ++-------------- components/Filters.tsx | 20 ++++++++++++++++++-- utils/types.ts | 8 +++++--- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/components/DownloadsComponent.tsx b/components/DownloadsComponent.tsx index 6dc6d01..4fb9036 100644 --- a/components/DownloadsComponent.tsx +++ b/components/DownloadsComponent.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { Box, CircularProgress, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'; import fetchFilteredReleases from "../utils/fetchReleases"; import { FilteredRelease, ArchEnums, OSEnums, SemverRegex } from '../utils/types'; -import {OperatingSystems, Architectures} from './Filters'; +import {OperatingSystems, Architectures, Versions} from './Filters'; import ReleasesTable from './ReleasesTable'; import data from '../public/static/releases-table-data.json'; import { DarkLightContainer } from '@/utils/darkLightContainer'; @@ -91,7 +91,6 @@ const DownloadsComponent: React.FC = () => { const filteredData = useMemo(() => { - console.log("update with", selectedOptions) const selectedArch = selectedOptions.arch; const filteredByVersion = structuredClone(originalData.filter((release) => release.version == selectedOptions.version)[0]) if (!filteredByVersion) { @@ -145,18 +144,7 @@ const DownloadsComponent: React.FC = () => { flexDirection: 'column', }, }}> - - Version - - + {setSelectedOptions((prev) => ({...prev, version: e.target.value}))}} versions={versions} value={selectedOptions.version}/> diff --git a/components/Filters.tsx b/components/Filters.tsx index 66ccdd2..1cca1c6 100644 --- a/components/Filters.tsx +++ b/components/Filters.tsx @@ -1,6 +1,22 @@ import React from 'react'; -import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; -import { compatibilityRules, ArchitecturesProps, OperatingSystemsProps } from '../utils/types'; +import { Box, ToggleButton, ToggleButtonGroup, Typography, Select, MenuItem, SelectChangeEvent } from '@mui/material'; +import { compatibilityRules, ArchitecturesProps, OperatingSystemsProps, VersionProps } from '../utils/types'; + + +export const Versions:React.FC = ({handleChange, value, versions}) => { + return ( + + Versions + + + ) +} export const Architectures:React.FC = ({handle, defaultArch, archs, defaultOs}) => { diff --git a/utils/types.ts b/utils/types.ts index 310a91c..bb2b700 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -1,7 +1,9 @@ +import { SelectChangeEvent } from "@mui/material"; + export interface VersionProps { - handle: (event: React.MouseEvent, newAlignment: string) => void; - defaultVersion: string; - data: Array; + handleChange: (event: SelectChangeEvent) => void; + value: string; + versions: string[]; } export interface ArchitecturesProps {