Skip to content
Merged
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
72 changes: 59 additions & 13 deletions app/Domains/Package/Actions/BuildPackageDownloadStatsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Domains\Organization\Contracts\Data\DailyDownloadData;
use App\Domains\Package\Contracts\Data\PackageDownloadStatsData;
use App\Domains\Package\Contracts\Data\VersionDownloadData;
use App\Domains\Package\Contracts\Data\VersionDailyDownloadData;
use App\Models\Package;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
Expand All @@ -31,22 +31,68 @@ public function handle(Package $package): PackageDownloadStatsData
);
}

$versionBreakdown = $package->downloads()
->select('version', DB::raw('COUNT(*) as downloads'))
->groupBy('version')
->orderByDesc('downloads')
->limit(10)
->get()
->map(fn ($row) => new VersionDownloadData(
version: $row->version,
downloads: (int) $row->getAttribute('downloads'),
))
->all();
$versionDailyDownloads = $this->buildVersionDailyDownloads($package, $startDate);

return new PackageDownloadStatsData(
totalDownloads: $package->downloads()->count(),
dailyDownloads: $dailyDownloads,
versionBreakdown: $versionBreakdown,
versionDailyDownloads: $versionDailyDownloads,
);
}

/**
* @return array<int, VersionDailyDownloadData>
*/
private function buildVersionDailyDownloads(Package $package, Carbon $startDate): array
{
$rows = $package->downloads()
->where('downloaded_at', '>=', $startDate)
->select(
DB::raw('DATE(downloaded_at) as date'),
'version',
DB::raw('COUNT(*) as downloads'),
)
->groupBy('date', 'version')
->get();

$topVersions = $rows
->groupBy('version')
->map(fn ($versionRows) => $versionRows->sum('downloads'))
->sortDesc()
->take(5)
->keys()
->all();

$hasOther = $rows->contains(fn ($row) => ! in_array($row->version, $topVersions));

$versionDailyMap = [];
foreach ($rows as $row) {
$version = in_array($row->version, $topVersions) ? $row->version : 'Other';
$date = (string) $row->getAttribute('date');
$versionDailyMap[$version][$date] = ($versionDailyMap[$version][$date] ?? 0) + (int) $row->getAttribute('downloads');
}

$orderedVersions = array_values(array_filter($topVersions, fn ($v) => isset($versionDailyMap[$v])));
if ($hasOther) {
$orderedVersions[] = 'Other';
}

$versionDailyDownloads = [];
foreach ($orderedVersions as $version) {
$dailyData = [];
for ($i = 29; $i >= 0; $i--) {
$date = Carbon::now()->subDays($i)->format('Y-m-d');
$dailyData[] = new DailyDownloadData(
date: $date,
downloads: $versionDailyMap[$version][$date] ?? 0,
);
}
$versionDailyDownloads[] = new VersionDailyDownloadData(
version: $version,
dailyDownloads: $dailyData,
);
}

return $versionDailyDownloads;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
use App\Domains\Organization\Contracts\Data\DailyDownloadData;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
use Spatie\TypeScriptTransformer\Attributes\TypeScriptType;

#[TypeScript]
class PackageDownloadStatsData extends Data
{
/**
* @param array<int, DailyDownloadData> $dailyDownloads
* @param array<int, VersionDownloadData> $versionBreakdown
* @param array<int, VersionDailyDownloadData> $versionDailyDownloads
*/
public function __construct(
public int $totalDownloads,
#[TypeScriptType('array<'.DailyDownloadData::class.'>')]
public array $dailyDownloads,
public array $versionBreakdown,
#[TypeScriptType('array<'.VersionDailyDownloadData::class.'>')]
public array $versionDailyDownloads,
) {}
}
21 changes: 21 additions & 0 deletions app/Domains/Package/Contracts/Data/VersionDailyDownloadData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Domains\Package\Contracts\Data;

use App\Domains\Organization\Contracts\Data\DailyDownloadData;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
use Spatie\TypeScriptTransformer\Attributes\TypeScriptType;

#[TypeScript]
class VersionDailyDownloadData extends Data
{
/**
* @param array<int, DailyDownloadData> $dailyDownloads
*/
public function __construct(
public string $version,
#[TypeScriptType('array<'.DailyDownloadData::class.'>')]
public array $dailyDownloads,
) {}
}
15 changes: 0 additions & 15 deletions app/Domains/Package/Contracts/Data/VersionDownloadData.php

This file was deleted.

2 changes: 1 addition & 1 deletion app/Domains/Package/Http/Controllers/PackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function show(Request $request, Organization $organization, Package $pack
$package->repository?->repo_identifier
));

$composerRepositoryUrl = url("/{$organization->slug}/packages.json");
$composerRepositoryUrl = url("/{$organization->slug}");

$versionUuid = $request->query('version');
$activeVersion = null;
Expand Down
13 changes: 3 additions & 10 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,10 @@ Requires webhook token verification via the `X-Gitlab-Token` header (plain text

## Configuring Composer

Add Pricore as a repository in your project's `composer.json`:
Add Pricore as a Composer repository:

```json
{
"repositories": [
{
"type": "composer",
"url": "https://pricore.yourcompany.com/your-org"
}
]
}
```bash
composer config repositories.your-org composer https://pricore.yourcompany.com/your-org
```

Then authenticate:
Expand Down
11 changes: 2 additions & 9 deletions docs/guide/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,8 @@ Versions are synced automatically when:

Add your Pricore organization as a Composer repository:

```json
{
"repositories": [
{
"type": "composer",
"url": "https://pricore.yourcompany.com/org/your-organization"
}
]
}
```bash
composer config repositories.your-organization composer https://pricore.yourcompany.com/your-organization
```

### 2. Authenticate
Expand Down
169 changes: 169 additions & 0 deletions resources/js/components/stats/version-download-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import { cn } from '@/lib/utils';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';

type VersionDailyDownloadData =
App.Domains.Package.Contracts.Data.VersionDailyDownloadData;
type DailyDownloadData =
App.Domains.Organization.Contracts.Data.DailyDownloadData;

interface VersionDownloadChartProps {
title: string;
versionData: VersionDailyDownloadData[];
fallbackData: DailyDownloadData[];
compact?: boolean;
className?: string;
}

const CHART_COLORS = [
'var(--chart-1)',
'var(--chart-2)',
'var(--chart-3)',
'var(--chart-4)',
'var(--chart-5)',
'var(--muted-foreground)',
] as const;

export function VersionDownloadChart({
title,
versionData,
fallbackData,
compact = false,
className,
}: VersionDownloadChartProps) {
const versions = versionData.map((v) => v.version);

const { chartData, chartConfig, maxDownloads } = useMemo(() => {
if (versions.length === 0) {
return {
chartData: [],
chartConfig: {} as ChartConfig,
maxDownloads: 0,
};
}

const config: ChartConfig = {};
versions.forEach((version, index) => {
config[version] = {
label: version,
color: CHART_COLORS[index % CHART_COLORS.length],
};
});

const dateCount = versionData[0].dailyDownloads.length;
const data: Record<string, string | number>[] = [];

for (let i = 0; i < dateCount; i++) {
const entry: Record<string, string | number> = {
date: versionData[0].dailyDownloads[i].date,
};
let dayTotal = 0;
for (const vd of versionData) {
entry[vd.version] = vd.dailyDownloads[i].downloads;
dayTotal += vd.dailyDownloads[i].downloads;
}
entry._total = dayTotal;
data.push(entry);
}

const max = Math.max(...data.map((d) => d._total as number));

return { chartData: data, chartConfig: config, maxDownloads: max };
}, [versionData, versions]);

const hasDownloads =
versions.length > 0
? chartData.some((d) => (d._total as number) > 0)
: fallbackData.some((d) => d.downloads > 0);

const yAxisWidth =
maxDownloads >= 10000 ? 55 : maxDownloads >= 1000 ? 48 : 40;

return (
<Card className={cn('flex flex-col', className)}>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent className="flex min-h-64 flex-1">
{!hasDownloads ? (
<div className="flex min-h-64 flex-1 items-center justify-center text-muted-foreground">
No downloads yet
</div>
) : (
<ChartContainer
config={chartConfig}
className="min-h-64 flex-1"
>
<BarChart
data={chartData}
margin={{ top: 4, right: 4, bottom: 0, left: 0 }}
barCategoryGap="25%"
>
<CartesianGrid
vertical={false}
strokeDasharray="3 3"
className="stroke-border"
/>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: string) =>
DateTime.fromISO(value).toFormat('LLL d')
}
interval={compact ? 4 : 2}
className="text-xs"
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
allowDecimals={false}
width={yAxisWidth}
className="text-xs"
/>
<ChartTooltip
content={
<ChartTooltipContent
config={chartConfig}
labelFormatter={(label: string) =>
DateTime.fromISO(label).toFormat(
'DDD',
)
}
/>
}
/>
{versions.map((version, index) => (
<Bar
key={version}
dataKey={version}
stackId="1"
fill={
CHART_COLORS[
index % CHART_COLORS.length
]
}
radius={
index === versions.length - 1
? [3, 3, 0, 0]
: [0, 0, 0, 0]
}
/>
))}
</BarChart>
</ChartContainer>
)}
</CardContent>
</Card>
);
}
Loading