Skip to content

feat: Build Stats #105

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

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/blog/[slug]/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const colourByType: Dict<string> = {

const textByType: Dict<string> = {
'announcement': 'Announcement',
'weekly': 'Weekly Update',
'weekly': 'Progress Update',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you forgot to rebase. This commmit already exists

'community': 'Community Highlight',
}

Expand Down
68 changes: 68 additions & 0 deletions app/changelog/buildStatsComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all components should be inside the outer components folder.

I'd say this component is an organism, so place it inside components/organisms/changelog/

import Build from '../../types/build';
import Panel from '../common/uiLibrary/panel';
import { FaArrowUp, FaWrench } from 'react-icons/fa';
import { FaCirclePlus } from "react-icons/fa6";
import Card from '../../components/mocules/Card';
import SmallNoteText from '../../components/mocules/SmallNoteText';

interface BuildStatsProps {
builds: Build[];
}

const BuildStatsComponent: React.FC<BuildStatsProps> = ({ builds }) => {
const recentBuilds = builds.slice(0, 150);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

150 is a big slice, isn't it?


const stats = recentBuilds.reduce((acc, build) => {
build.changes.forEach(change => {
switch (change.category) {
case 'NEW':
acc.additions++;
break;
case 'IMPROVEMENT':
acc.improvements++;
break;
case 'FIX':
acc.fixes++;
break;
default:
break;
}
});
return acc;
}, { additions: 0, improvements: 0, fixes: 0 });

return (
<Panel>
<h3 className="text-xl font-bold mb-4 text-white">Recent Build Statistics</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">

there is an awkward transition if you test how responsive is this new component where the 3 cols are not big enough for the text

<Card>
<FaCirclePlus className="text-green-500 text-2xl mr-3" />
<div>
<p className="text-gray-300 text-sm">Additions</p>
<p className="text-white text-xl font-bold">{stats.additions}</p>
</div>
</Card>

<Card>
<FaArrowUp className="text-blue-600 text-2xl mr-3" />
<div>
<p className="text-gray-300 text-sm">Improvements</p>
<p className="text-white text-xl font-bold">{stats.improvements}</p>
</div>
</Card>

<Card>
<FaWrench className="text-orange-600 text-2xl mr-3" />
<div>
<p className="text-gray-300 text-sm">Bug Fixes</p>
<p className="text-white text-xl font-bold">{stats.fixes}</p>
</div>
</Card>
</div>
<SmallNoteText text={`Stats are based on the past ${recentBuilds.length} builds that were pushed onto our staging server from upstream.`} />
</Panel>
);
};

export default BuildStatsComponent;
32 changes: 32 additions & 0 deletions app/changelog/latestReleaseComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import Panel from '../common/uiLibrary/panel';
import { FaTag } from 'react-icons/fa';
import Card from '../../components/mocules/Card';
import SmallNoteText from '../../components/mocules/SmallNoteText';

interface ReleaseInfo {
version: string;
commitSha: string;
}

interface LatestReleaseComponentProps {
releaseInfo: ReleaseInfo;
}

const LatestReleaseComponent: React.FC<LatestReleaseComponentProps> = ({ releaseInfo }) => {
return (
<Panel>
<h3 className="text-xl font-bold mb-4 text-white">Latest Code-Scan Release</h3>
<Card>
<FaTag className="text-purple-500 text-2xl mr-3" />
<div>
<p className="text-white text-xl font-bold">{releaseInfo.version}</p>
<p className="text-gray-300 text-sm">Commit: {releaseInfo.commitSha.substring(0, 7)}</p>
</div>
</Card>
<SmallNoteText text="To keep our players safe, we only allow whitelisted code to be run on clients that are scanned and opened via stationhub." />
</Panel>
);
};

export default LatestReleaseComponent;
99 changes: 91 additions & 8 deletions app/changelog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,97 @@ import Container from "../common/uiLibrary/container";
import PageHeading from "../common/uiLibrary/PageHeading";
import BuildComponent from "./buildComponent";
import LoadingBuild from "./loading";
import BuildStatsComponent from "./buildStatsComponent";
import LatestReleaseComponent from "./latestReleaseComponent";

interface ReleaseInfo {
version: string;
commitSha: string;
}

interface GitHubTag {
name: string;
commit: {
url: string;
sha: string;
};
}

const fetchChangelog = async (url: string): Promise<AllChangesResponse> => {
const response = await fetch(url);
return await response.json();
}

const fetchLatestRelease = async (): Promise<ReleaseInfo | null> => {
try {
const response = await fetch('https://api.github.com/repos/unitystation/unitystation/tags');
const data = await response.json();

if (data && data.length > 0) {
const goodFileTags = data
.filter((tag: GitHubTag) => tag.name.startsWith('good-file-'))
.sort((a: GitHubTag, b: GitHubTag) => {
const versionA = a.name.replace('good-file-', '').split('.').map(Number);
const versionB = b.name.replace('good-file-', '').split('.').map(Number);
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
const numA = versionA[i] || 0;
const numB = versionB[i] || 0;
if (numA !== numB) {
return numB - numA; // Higher version first
}
}
return 0;
});

if (goodFileTags.length > 0) {
const latestTag = goodFileTags[0];
const commitResponse = await fetch(latestTag.commit.url);
const commitData = await commitResponse.json();

return {
version: latestTag.name,
commitSha: latestTag.commit.sha,
};
}
}
return null;
} catch (error) {
console.error('Error fetching latest release:', error);
return null;
}
};

const ChangelogPage = () => {
const [buildsResponse, setBuildsResponse] = useState<AllChangesResponse>();
const [builds, setBuilds] = useState<Build[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const INITIAL_PAGE = "https://changelog.unitystation.org/all-changes?limit=5";
const [releaseInfo, setReleaseInfo] = useState<ReleaseInfo | null>(null);
const [isLoadingRelease, setIsLoadingRelease] = useState<boolean>(true);
const INITIAL_PAGE = "https://changelog.unitystation.org/all-changes?limit=10";

useEffect(() => {
fetchChangelog(INITIAL_PAGE).then((response) => {
const fetchData = async () => {
setIsLoading(true);
setBuildsResponse(response);
setBuilds(response.results);
}).finally(
() => setIsLoading(false)
)
setIsLoadingRelease(true);

try {
const [changelogResponse, releaseData] = await Promise.all([
fetchChangelog(INITIAL_PAGE),
fetchLatestRelease()
]);

setBuildsResponse(changelogResponse);
setBuilds(changelogResponse.results);
setReleaseInfo(releaseData);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setIsLoading(false);
setIsLoadingRelease(false);
}
};

fetchData();
}, []);

const handleScroll = useCallback(async () => {
Expand All @@ -46,8 +117,20 @@ const ChangelogPage = () => {

return (
<Container>
<PageHeading isCentered>Changelog</PageHeading>
<div className="flex flex-col gap-4">
{!!builds && builds.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<BuildStatsComponent builds={builds} />
{isLoadingRelease ? (
<div className="flex items-center justify-center h-32 bg-gray-800 rounded-lg">
<p className="text-gray-400">Loading latest code-scan release info...</p>
</div>
) : releaseInfo ? (
<LatestReleaseComponent releaseInfo={releaseInfo} />
) : null}
</div>
)}
<PageHeading isCentered>Changelog</PageHeading>
{!!builds && builds.map((build, index) => {
return <BuildComponent build={build} key={index}/>
})}
Expand Down
15 changes: 15 additions & 0 deletions components/mocules/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mocules?


interface CardProps {
children: ReactNode;
}

const Card: React.FC<CardProps> = ({ children }) => {
return (
<div className="flex items-center p-4 bg-gray-800 rounded-lg">
{children}
</div>
);
};

export default Card;
13 changes: 13 additions & 0 deletions components/mocules/SmallNoteText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

interface SmallNoteTextProps {
text: string;
}

const SmallNoteText: React.FC<SmallNoteTextProps> = ({ text }) => {
return (
<p className="text-gray-400 text-sm mt-4">{text}</p>
);
};

export default SmallNoteText;
Loading