-
Notifications
You must be signed in to change notification settings - Fork 1
Add institutions, contributions, and third party information to the About window #229
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
Changes from all commits
6742182
cd1dc3c
b58d362
384e46c
92a6c2a
956cc1d
5765c3e
b782237
61cdf63
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 |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| /** | ||
| * Preprocessing script that parses a third-party license markdown file and | ||
| * generates a static TypeScript data file at src/data/ThirdPartyDependencies.ts. | ||
| * | ||
| * Usage: | ||
| * npx tsx scripts/generateThirdParty.ts <path-to-license.md> [output-path] | ||
| * | ||
| * Example: | ||
| * npx tsx scripts/generateThirdParty.ts LICENSES.md | ||
| * npx tsx scripts/generateThirdParty.ts LICENSES.md src/data/ThirdPartyDependencies.ts | ||
| * | ||
| * Expected input format: | ||
| * A markdown file where each library has a section like: | ||
| * | ||
| * ## Library Name | ||
| * | ||
| * **License:** MIT | ||
| * **URL:** https://example.com | ||
| * | ||
| * <full license text> | ||
| */ | ||
|
|
||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| interface Dependency { | ||
| name: string; | ||
| license: string; | ||
| url: string; | ||
| licenseText: string; | ||
| } | ||
|
|
||
| function parseMarkdown(content: string): Dependency[] { | ||
| // Split into sections on lines that start a new ## heading. | ||
| // The first chunk is the intro/table — skip it. | ||
| const chunks = content.split(/\n(?=## )/); | ||
|
|
||
| const dependencies: Dependency[] = []; | ||
|
|
||
| for (const chunk of chunks.slice(1)) { | ||
| const lines = chunk.split('\n'); | ||
|
|
||
| const name = lines[0].replace(/^##\s+/, '').trim(); | ||
|
|
||
| const licenseMatch = chunk.match(/^\*\*License:\*\*\s*(.+)$/m); | ||
| const urlMatch = chunk.match(/^\*\*URL:\*\*\s*(.+)$/m); | ||
|
|
||
| if (!licenseMatch || !urlMatch) { | ||
| console.warn(`Skipping section "${name}": missing License or URL field.`); | ||
| continue; | ||
| } | ||
|
|
||
| const license = licenseMatch[1].trim(); | ||
| const url = urlMatch[1].trim(); | ||
|
|
||
| // License text is everything after the **URL:** line. | ||
| const urlLineIndex = lines.findIndex((l) => l.startsWith('**URL:**')); | ||
| const licenseText = lines | ||
| .slice(urlLineIndex + 1) | ||
| .join('\n') | ||
| .trim(); | ||
|
|
||
| dependencies.push({ name, license, url, licenseText }); | ||
| } | ||
|
|
||
| return dependencies; | ||
| } | ||
|
|
||
| function generateTypeScript(dependencies: Dependency[]): string { | ||
| const entries = dependencies | ||
| .map((dep) => { | ||
| return [ | ||
| ` {`, | ||
| ` name: ${JSON.stringify(dep.name)},`, | ||
| ` license: ${JSON.stringify(dep.license)},`, | ||
| ` url: ${JSON.stringify(dep.url)},`, | ||
| ` licenseText: ${JSON.stringify(dep.licenseText)}`, | ||
| ` }` | ||
| ].join('\n'); | ||
| }) | ||
| .join(',\n'); | ||
|
|
||
| return `// This file is auto-generated. Do not edit manually. | ||
| // To regenerate, run: | ||
| // npx tsx scripts/generateThirdParty.ts <path-to-license.md> | ||
|
|
||
| export interface ThirdPartyDependency { | ||
| name: string; | ||
| license: string; | ||
| url: string; | ||
| licenseText: string; | ||
| } | ||
|
|
||
| export const ThirdPartyDependencies: ThirdPartyDependency[] = [ | ||
| ${entries} | ||
| ]; | ||
| `; | ||
| } | ||
|
|
||
| async function main() { | ||
| const inputPath = process.argv[2]; | ||
| if (!inputPath) { | ||
| console.error('Error: No input file specified.'); | ||
| console.error('Usage: npx tsx scripts/generateThirdParty.ts <path-to-license.md>'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const resolvedInput = path.resolve(inputPath); | ||
| if (!fs.existsSync(resolvedInput)) { | ||
| console.error(`Error: File not found: ${resolvedInput}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const outputPath = path.resolve( | ||
| process.argv[3] ?? 'src/data/ThirdPartyDependencies.ts' | ||
| ); | ||
|
|
||
| const content = fs.readFileSync(resolvedInput, 'utf-8'); | ||
| const dependencies = parseMarkdown(content); | ||
|
|
||
| if (dependencies.length === 0) { | ||
| console.error('Error: No dependency sections found in the input file.'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const output = generateTypeScript(dependencies); | ||
|
|
||
| fs.mkdirSync(path.dirname(outputPath), { recursive: true }); | ||
| fs.writeFileSync(outputPath, output, 'utf-8'); | ||
|
|
||
|
alexanderbock marked this conversation as resolved.
|
||
| console.log(`Parsed ${dependencies.length} dependencies.`); | ||
| console.log(`Written to: ${outputPath}`); | ||
| } | ||
|
|
||
| main(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,25 @@ | ||
| import { useEffect } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { Anchor, Grid, Image, Modal, Stack, Text, Title } from '@mantine/core'; | ||
| import { | ||
| Anchor, | ||
| Avatar, | ||
| Box, | ||
| Code, | ||
| Container, | ||
| Group, | ||
| Image, | ||
| Modal, | ||
| SimpleGrid, | ||
| Stack, | ||
| Text, | ||
| Title | ||
| } from '@mantine/core'; | ||
| import { SemanticVersion } from 'openspace-api-js/types'; | ||
|
|
||
| import { useAppSelector } from '@/redux/hooks'; | ||
| import { Collapsable } from '@/components/Collapsable/Collapsable'; | ||
| import { ThirdPartyDependencies } from '@/data/ThirdPartyDependencies'; | ||
| import { getContributors } from '@/redux/contributors/contributorsMiddleware'; | ||
| import { useAppDispatch, useAppSelector } from '@/redux/hooks'; | ||
|
Comment on lines
+19
to
+22
Member
Author
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. Ehm.. Help?
Collaborator
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. I don't know how much of a problem this actually is based on the size of the dependencies list, but it makes a good point. The about page is rarely loaded, but now every user pays the "cost" of statically loading this data file. In this case, it seems to suggest loading this data as part of the import { useEffect, useState } from "react";
export function About({ opened }) {
const [licenses, setLicenses] = useState(null);
useEffect(() => {
if (!opened || licenses) {
return;
}
async function load() {
const module = await import("@/data/ThirdPartyDependencies");
setLicenses(module.ThirdPartyDependencies);
}
load();
}, [opened, licenses]);
return (
// Other rendering code...
<Title order={3} mt={'md'} mb={'xs'}>
{t('third-party-title')}
</Title>
{ licenses && /** Render the licence list **/ }
);
}An alternative could be to move the third-party licences part into a separate component, and then use // This component statically loads a large json file containing all third party
// dependencies and their licenses. To avoid loading this file when the About modal is
// not open, we lazy load the component that renders the list of dependencies
const ThirdPartyDependenciesList = React.lazy(() =>
import('./ThirdPartyDependenciesList').then((module) => ({
default: module.ThirdPartyDependenciesList
}))
);
// In the render function:
<Title order={3} mt={'md'} mb={'xs'}>
{t('third-party-title')}
</Title>
<Suspense fallback={<Loader />}>
<ThirdPartyDependenciesList />
</Suspense>
Member
Author
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. If it is not that big of a deal (file is about 200 lines and not going to grow that much more), I guess we can keep it as it is as well |
||
|
|
||
| interface Props { | ||
| opened: boolean; | ||
|
|
@@ -12,7 +29,16 @@ interface Props { | |
| export function About({ opened, close }: Props) { | ||
| const { t } = useTranslation('components', { keyPrefix: 'about' }); | ||
|
|
||
| const dispatch = useAppDispatch(); | ||
| const openSpaceVersion = useAppSelector((state) => state.version.openSpaceVersion); | ||
| const contributors = useAppSelector((state) => state.contributors.contributors); | ||
| const contributorsStatus = useAppSelector((state) => state.contributors.status); | ||
|
|
||
| useEffect(() => { | ||
| if (opened && (contributorsStatus === 'idle' || contributorsStatus === 'failed')) { | ||
| dispatch(getContributors()); | ||
| } | ||
| }, [opened, contributorsStatus, dispatch]); | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| function osVersionNumber(): string { | ||
| if (!openSpaceVersion) { | ||
|
|
@@ -33,31 +59,113 @@ export function About({ opened, close }: Props) { | |
| opened={opened} | ||
| onClose={close} | ||
| title={t('modal-title')} | ||
| size={'40%'} | ||
| size={'xxl'} | ||
| closeButtonProps={{ 'aria-label': 'Close about' }} | ||
| scrollAreaComponent={Modal.NativeScrollArea} | ||
|
alexanderbock marked this conversation as resolved.
|
||
| > | ||
| <Grid> | ||
| <Grid.Col span={4}> | ||
| <Image | ||
| src={`${import.meta.env.BASE_URL}/images/openspace-logo.png`} | ||
| alt={t('logo-alt-text')} | ||
| w={'100%'} | ||
| /> | ||
| </Grid.Col> | ||
| <Grid.Col span={8}> | ||
| <Container px={'xs'}> | ||
| <Group wrap={'nowrap'} align={'top'}> | ||
| <Box px={'xs'}> | ||
| <Image | ||
| src={`${import.meta.env.BASE_URL}/images/openspace-logo.png`} | ||
| alt={t('logo-alt-text')} | ||
| w={{ xs: 100, sm: 180 }} | ||
| fit={'contain'} | ||
| /> | ||
| </Box> | ||
| <Stack gap={'xs'}> | ||
| <Title order={1}>OpenSpace</Title> | ||
| <Text>{t('about-openspace-description')}</Text> | ||
| <Text>{osVersionNumber()}</Text> | ||
| <Text> | ||
| © 2014 - {new Date().getFullYear()} OpenSpace Development Team | ||
| </Text> | ||
| <Anchor href={'https://www.openspaceproject.com/'} target={'_blank'}> | ||
| <Anchor | ||
| href={'https://www.openspaceproject.com/'} | ||
| target={'_blank'} | ||
| rel={'noopener noreferrer'} | ||
| > | ||
| openspaceproject.com | ||
| </Anchor> | ||
| <Text>{t('openspace-institutions')}</Text> | ||
| </Stack> | ||
| </Grid.Col> | ||
| </Grid> | ||
| </Group> | ||
|
|
||
| <Title order={3}>{t('contributors-title')}</Title> | ||
| <div style={{ marginTop: 'var(--mantine-spacing-xs)' }}> | ||
| {contributorsStatus === 'loading' && ( | ||
| <Text c={'dimmed'}>{t('contributors-loading')}</Text> | ||
| )} | ||
| {contributorsStatus === 'failed' && ( | ||
| <Text c={'red'}>{t('contributors-error')}</Text> | ||
| )} | ||
| {contributorsStatus === 'succeeded' && ( | ||
| <SimpleGrid cols={{ base: 6, xs: 10, sm: 12, md: 14 }} spacing={'xs'}> | ||
| {contributors.map((contributor) => ( | ||
| <Anchor | ||
| key={contributor.login} | ||
| href={contributor.htmlUrl} | ||
| target={'_blank'} | ||
| rel={'noopener noreferrer'} | ||
| underline={'never'} | ||
| c={'inherit'} | ||
| > | ||
| <Stack align={'center'} gap={4}> | ||
| <Avatar | ||
| src={contributor.avatarUrl} | ||
| alt={contributor.login} | ||
| size={'md'} | ||
| /> | ||
| <Text size={'xs'} ta={'center'} style={{ wordBreak: 'break-all' }}> | ||
| {contributor.login} | ||
| </Text> | ||
| </Stack> | ||
| </Anchor> | ||
| ))} | ||
| </SimpleGrid> | ||
| )} | ||
| </div> | ||
|
|
||
| <Title order={3} mt={'md'} mb={'xs'}> | ||
| {t('third-party-title')} | ||
| </Title> | ||
| <Stack gap={0}> | ||
| {ThirdPartyDependencies.map((dep) => ( | ||
| <Collapsable key={dep.name} title={dep.name} noTransition> | ||
| <Stack gap={'xs'} pb={'xs'}> | ||
| <Text size={'sm'}> | ||
| <Text span fw={600}> | ||
| {t('third-party-license-label')}:{' '} | ||
| </Text> | ||
| {dep.license} | ||
| </Text> | ||
| <Text size={'sm'}> | ||
| <Text span fw={600}> | ||
| {t('third-party-url-label')}:{' '} | ||
| </Text> | ||
| <Anchor | ||
| href={dep.url} | ||
| target={'_blank'} | ||
| rel={'noopener noreferrer'} | ||
| size={'sm'} | ||
| > | ||
| {dep.url} | ||
| </Anchor> | ||
| </Text> | ||
| <Code | ||
| block | ||
| style={{ | ||
| whiteSpace: 'pre-wrap', | ||
| fontSize: 'var(--mantine-font-size-xs)' | ||
| }} | ||
| > | ||
| {dep.licenseText} | ||
| </Code> | ||
| </Stack> | ||
| </Collapsable> | ||
| ))} | ||
| </Stack> | ||
| </Container> | ||
| </Modal> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.