mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-26 10:35:04 +03:00
[MAGE] Implement filter-dependent stats (#1490)
This commit is contained in:
parent
fb39f86ce9
commit
3c45043728
@ -1,6 +1,6 @@
|
|||||||
app waspAi {
|
app waspAi {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.5"
|
||||||
},
|
},
|
||||||
title: "MAGE - GPT Web App Generator ✨",
|
title: "MAGE - GPT Web App Generator ✨",
|
||||||
head: [
|
head: [
|
||||||
|
@ -45,43 +45,7 @@ export function Stats() {
|
|||||||
}, {});
|
}, {});
|
||||||
}, [stats]);
|
}, [stats]);
|
||||||
|
|
||||||
function getColorValue(colorName) {
|
const filteredProjects = useMemo(() => {
|
||||||
return availableColors.find((color) => color.name === colorName).color;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusName(status) {
|
|
||||||
switch (status) {
|
|
||||||
case "in-progress":
|
|
||||||
return "inProgress";
|
|
||||||
case "success":
|
|
||||||
return "success";
|
|
||||||
case "failure":
|
|
||||||
return "error";
|
|
||||||
case "cancelled":
|
|
||||||
return "cancelled";
|
|
||||||
default:
|
|
||||||
return "idle";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusText(status) {
|
|
||||||
switch (status) {
|
|
||||||
case "in-progress":
|
|
||||||
return "In progress";
|
|
||||||
case "success":
|
|
||||||
return "Success";
|
|
||||||
case "failure":
|
|
||||||
return "Error";
|
|
||||||
case "cancelled":
|
|
||||||
return "Cancelled";
|
|
||||||
case "pending":
|
|
||||||
return "Pending";
|
|
||||||
default:
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredStats = useMemo(() => {
|
|
||||||
const filters = [];
|
const filters = [];
|
||||||
if (filterOutExampleApps) {
|
if (filterOutExampleApps) {
|
||||||
filters.push(
|
filters.push(
|
||||||
@ -103,9 +67,9 @@ export function Stats() {
|
|||||||
: [];
|
: [];
|
||||||
}, [stats, stats?.projects, filterOutExampleApps, filterOutKnownUsers]);
|
}, [stats, stats?.projects, filterOutExampleApps, filterOutKnownUsers]);
|
||||||
|
|
||||||
const limitedFilteredStats = useMemo(() => {
|
const limitedFilteredProjects = useMemo(() => {
|
||||||
return filteredStats.slice(0, 1000);
|
return filteredProjects.slice(0, 1000);
|
||||||
}, [filteredStats]);
|
}, [filteredProjects]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <p>Loading</p>;
|
return <p>Loading</p>;
|
||||||
@ -119,41 +83,11 @@ export function Stats() {
|
|||||||
return <p>Couldn't load stats</p>;
|
return <p>Couldn't load stats</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadStats = getDownloadStats(filteredProjects);
|
||||||
|
|
||||||
const downloadedPercentage =
|
const downloadedPercentage =
|
||||||
Math.round(stats.downloadStats.downloadRatio * 10000) / 100;
|
Math.round(downloadStats.downloadRatio * 10000) / 100;
|
||||||
|
|
||||||
function getFormattedDiff(start, end) {
|
|
||||||
const diff = (end - start) / 1000;
|
|
||||||
const minutes = Math.round(diff / 60);
|
|
||||||
const remainingSeconds = Math.round(diff % 60);
|
|
||||||
return `${minutes}m ${remainingSeconds}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDuration(stat) {
|
|
||||||
if (!logsByProjectId[stat.id]) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
const logs = logsByProjectId[stat.id];
|
|
||||||
if (logs.length < 2) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
const start = logs[logs.length - 1].createdAt;
|
|
||||||
const end = logs[0].createdAt;
|
|
||||||
return getFormattedDiff(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWaitingInQueueDuration(stat) {
|
|
||||||
if (!logsByProjectId[stat.id]) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
const logs = logsByProjectId[stat.id];
|
|
||||||
if (logs.length < 2) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
const start = stat.createdAt;
|
|
||||||
const end = logs[logs.length - 1].createdAt;
|
|
||||||
return getFormattedDiff(start, end);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
@ -192,7 +126,7 @@ export function Stats() {
|
|||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<BarChart
|
<BarChart
|
||||||
chartType={chartType.value}
|
chartType={chartType.value}
|
||||||
projects={filteredStats}
|
projects={filteredProjects}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
@ -242,12 +176,12 @@ export function Stats() {
|
|||||||
<span className="bg-slate-100 rounded-md px-2 py-1">
|
<span className="bg-slate-100 rounded-md px-2 py-1">
|
||||||
Generated:{" "}
|
Generated:{" "}
|
||||||
<strong className="text-slate-800">
|
<strong className="text-slate-800">
|
||||||
{filteredStats.length}
|
{filteredProjects.length}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-slate-100 rounded-md px-2 py-1">
|
<span className="bg-slate-100 rounded-md px-2 py-1">
|
||||||
Downlaoded:{" "}
|
Downlaoded:{" "}
|
||||||
<strong className="text-slate-800">{`${stats.downloadStats.projectsDownloaded} (${downloadedPercentage}%)`}</strong>
|
<strong className="text-slate-800">{`${downloadStats.projectsDownloaded} (${downloadedPercentage}%)`}</strong>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -275,29 +209,29 @@ export function Stats() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{limitedFilteredStats.map((stat) => (
|
{filteredProjects.map((project) => (
|
||||||
<tr className="bg-white border-b" key={stat.id}>
|
<tr className="bg-white border-b" key={project.id}>
|
||||||
<th
|
<th
|
||||||
scope="row"
|
scope="row"
|
||||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap flex items-center gap-2"
|
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Color value={getColorValue(stat.primaryColor)} />{" "}
|
<Color value={getColorValue(project.primaryColor)} />{" "}
|
||||||
<span
|
<span
|
||||||
title={stat.description}
|
title={project.description}
|
||||||
className="max-w-[250px] overflow-hidden overflow-ellipsis"
|
className="max-w-[250px] overflow-hidden overflow-ellipsis"
|
||||||
>
|
>
|
||||||
{stat.name}
|
{project.name}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
{stat.user && (
|
{project.user && (
|
||||||
<span title={stat.user.email}>
|
<span title={project.user.email}>
|
||||||
<WaspIcon className="w-5 h-5" />
|
<WaspIcon className="w-5 h-5" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{stat.zipDownloadedAt && (
|
{project.zipDownloadedAt && (
|
||||||
<span
|
<span
|
||||||
title={`Downlaoded ${format(
|
title={`Downlaoded ${format(
|
||||||
stat.zipDownloadedAt
|
project.zipDownloadedAt
|
||||||
)}`}
|
)}`}
|
||||||
className="w-5 h-5 bg-sky-100 rounded-full flex items-center justify-center text-sky-800 border border-sky-200"
|
className="w-5 h-5 bg-sky-100 rounded-full flex items-center justify-center text-sky-800 border border-sky-200"
|
||||||
>
|
>
|
||||||
@ -307,28 +241,28 @@ export function Stats() {
|
|||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<StatusPill status={getStatusName(stat.status)} sm>
|
<StatusPill status={getStatusName(project.status)} sm>
|
||||||
{getStatusText(stat.status)}
|
{getStatusText(project.status)}
|
||||||
</StatusPill>
|
</StatusPill>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-6 py-4"
|
className="px-6 py-4"
|
||||||
title={`${stat.createdAt.toLocaleDateString()} ${stat.createdAt.toLocaleTimeString()}`}
|
title={`${project.createdAt.toLocaleDateString()} ${project.createdAt.toLocaleTimeString()}`}
|
||||||
>
|
>
|
||||||
{format(stat.createdAt)}
|
{format(project.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
{getWaitingInQueueDuration(stat)} →{" "}
|
{getWaitingInQueueDuration(project, logsByProjectId)}{" "}
|
||||||
{getDuration(stat)}
|
→ {getDuration(project, logsByProjectId)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className={`px-6 py-4 creativity-${stat.creativityLevel}`}
|
className={`px-6 py-4 creativity-${project.creativityLevel}`}
|
||||||
>
|
>
|
||||||
{stat.creativityLevel}
|
{project.creativityLevel}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<Link
|
<Link
|
||||||
to={`/result/${stat.id}`}
|
to={`/result/${project.id}`}
|
||||||
className="font-medium text-sky-600 hover:underline"
|
className="font-medium text-sky-600 hover:underline"
|
||||||
>
|
>
|
||||||
View the app →
|
View the app →
|
||||||
@ -338,7 +272,7 @@ export function Stats() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{filteredStats.length > limitedFilteredStats.length && (
|
{filteredProjects.length > limitedFilteredProjects.length && (
|
||||||
<div className="relative px-6 py-3 bg-gray-50 text-sm text-slate-500 text-center">
|
<div className="relative px-6 py-3 bg-gray-50 text-sm text-slate-500 text-center">
|
||||||
Showing only the latest 1000 projects
|
Showing only the latest 1000 projects
|
||||||
</div>
|
</div>
|
||||||
@ -350,3 +284,91 @@ export function Stats() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDownloadStats(projects) {
|
||||||
|
const projectsAfterDownloadTracking = projects.filter(
|
||||||
|
(project) =>
|
||||||
|
// This is the time of the first recorded download (after we rolled out download tracking).
|
||||||
|
project.createdAt > new Date("2023-07-14 10:36:45.12") &&
|
||||||
|
project.status === "success"
|
||||||
|
);
|
||||||
|
const downloadedProjects = projectsAfterDownloadTracking.filter(
|
||||||
|
(project) => project.zipDownloadedAt !== null
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
projectsDownloaded: downloadedProjects.length,
|
||||||
|
downloadRatio:
|
||||||
|
projectsAfterDownloadTracking.length > 0
|
||||||
|
? downloadedProjects.length / projectsAfterDownloadTracking.length
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedDiff(start, end) {
|
||||||
|
const diff = (end - start) / 1000;
|
||||||
|
const minutes = Math.round(diff / 60);
|
||||||
|
const remainingSeconds = Math.round(diff % 60);
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDuration(stat, logsByProjectId) {
|
||||||
|
if (!logsByProjectId[stat.id]) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const logs = logsByProjectId[stat.id];
|
||||||
|
if (logs.length < 2) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const start = logs[logs.length - 1].createdAt;
|
||||||
|
const end = logs[0].createdAt;
|
||||||
|
return getFormattedDiff(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWaitingInQueueDuration(stat, logsByProjectId) {
|
||||||
|
if (!logsByProjectId[stat.id]) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const logs = logsByProjectId[stat.id];
|
||||||
|
if (logs.length < 2) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const start = stat.createdAt;
|
||||||
|
const end = logs[logs.length - 1].createdAt;
|
||||||
|
return getFormattedDiff(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorValue(colorName) {
|
||||||
|
return availableColors.find((color) => color.name === colorName).color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusName(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "in-progress":
|
||||||
|
return "inProgress";
|
||||||
|
case "success":
|
||||||
|
return "success";
|
||||||
|
case "failure":
|
||||||
|
return "error";
|
||||||
|
case "cancelled":
|
||||||
|
return "cancelled";
|
||||||
|
default:
|
||||||
|
return "idle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "in-progress":
|
||||||
|
return "In progress";
|
||||||
|
case "success":
|
||||||
|
return "Success";
|
||||||
|
case "failure":
|
||||||
|
return "Error";
|
||||||
|
case "cancelled":
|
||||||
|
return "Cancelled";
|
||||||
|
case "pending":
|
||||||
|
return "Pending";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -215,12 +215,9 @@ export const getStats = (async (_args, context) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadStats = getDownloadStats(projects);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
latestProjectsWithLogs,
|
latestProjectsWithLogs,
|
||||||
downloadStats,
|
|
||||||
};
|
};
|
||||||
}) satisfies GetStats<{}>;
|
}) satisfies GetStats<{}>;
|
||||||
|
|
||||||
@ -229,20 +226,3 @@ export const getNumProjects = (async (_args, context) => {
|
|||||||
const numProjects = await Project.count();
|
const numProjects = await Project.count();
|
||||||
return numProjects;
|
return numProjects;
|
||||||
}) satisfies GetNumProjects<{}>;
|
}) satisfies GetNumProjects<{}>;
|
||||||
|
|
||||||
function getDownloadStats(projects: Project[]) {
|
|
||||||
const projectsAfterDownloadTracking = projects.filter(
|
|
||||||
(project) =>
|
|
||||||
// This is the time of the first recorded download (after we rolled out download tracking).
|
|
||||||
project.createdAt > new Date("2023-07-14 10:36:45.12") &&
|
|
||||||
project.status === "success"
|
|
||||||
);
|
|
||||||
const downloadedProjects = projectsAfterDownloadTracking.filter(
|
|
||||||
(project) => project.zipDownloadedAt !== null
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
projectsDownloaded: downloadedProjects.length,
|
|
||||||
downloadRatio:
|
|
||||||
downloadedProjects.length / projectsAfterDownloadTracking.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user