Skip to content
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
18 changes: 15 additions & 3 deletions src/api/MinerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,24 @@ export const useMinerIssues = (githubId: string, enabled?: boolean) =>
);

/**
* Fan-out variant: one mirror-API call per miner, useful for the watchlist.
* Fan-out variant: one mirror-API call per miner, useful for the watchlist
* and the dashboard issues-trend aggregation.
*
* `since` (ISO timestamp) is forwarded to the mirror as a query param. Omit
* for the mirror's default 35-day window — this also keeps the cache key
* stable across callers that don't need a custom range.
*/
export const useMinersIssues = (githubIds: string[], enabled?: boolean) =>
export const useMinersIssues = (
githubIds: string[],
enabled?: boolean,
since?: string,
) =>
useMirrorApiQueries<MinerIssuesResponse, MinerIssue[]>(
'useMinerIssues',
githubIds.map((id) => `/miners/${id}/issues`),
githubIds.map((id) => {
const path = `/miners/${id}/issues`;
return since ? `${path}?since=${encodeURIComponent(since)}` : path;
}),
{
enabled,
select: (data) => data?.issues ?? [],
Expand Down
36 changes: 32 additions & 4 deletions src/pages/dashboard/dashboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import {
type CommitLog,
type MinerEvaluation,
type MinerIssue,
type Repository,
} from '../../api';
import { type IssueBounty } from '../../api/models/Issues';
Expand Down Expand Up @@ -170,6 +171,33 @@ export const getPreviousWindowBounds = (
};
};

// Omitting `since` uses the mirror's default 35-day window and keeps the
// cache key stable across 1d/7d/35d ranges.
export const getMirrorSinceParam = (
range: TrendTimeRange,
): string | undefined =>
range === 'all' ? new Date(GITTENSOR_START_MS).toISOString() : undefined;

// Dedupe by (repo, number) so an issue surfaced under multiple miners is counted once.
export const flattenMinerIssues = (
responses: ReadonlyArray<ReadonlyArray<MinerIssue>>,
): MinerIssue[] => {
const seen = new Set<string>();
const flattened: MinerIssue[] = [];
responses.forEach((batch) => {
batch.forEach((issue) => {
const key = `${issue.repo_full_name}#${issue.issue_number}`;
if (seen.has(key)) return;
seen.add(key);
flattened.push(issue);
});
});
return flattened;
};

export const isResolvedMinerIssue = (issue: MinerIssue): boolean =>
issue.state === 'CLOSED' && issue.state_reason === 'COMPLETED';

const getUtcWeekStart = (timestamp: number) => {
const date = new Date(timestamp);
const dayOfWeek = date.getUTCDay();
Expand Down Expand Up @@ -278,18 +306,18 @@ const formatDelta = (

export const buildDashboardTrendData = (
prs: CommitLog[],
issues: IssueBounty[],
issues: MinerIssue[],
range: TrendTimeRange,
now = new Date(),
): { labels: string[]; series: DashboardTrendSeries[] } => {
const mergedPrTimestamps = prs.map((pr) => toTimestamp(pr.mergedAt));
const openedPrTimestamps = prs.map((pr) => toTimestamp(pr.prCreatedAt));
const openedIssueTimestamps = issues.map((issue) =>
toTimestamp(issue.createdAt),
toTimestamp(issue.created_at),
);
const resolvedIssueTimestamps = issues
.filter((issue) => issue.status === 'completed')
.map((issue) => toTimestamp(issue.completedAt));
.filter(isResolvedMinerIssue)
.map((issue) => toTimestamp(issue.closed_at));
const buckets = buildTrendBuckets(
[
...mergedPrTimestamps,
Expand Down
58 changes: 53 additions & 5 deletions src/pages/dashboard/useDashboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import {
useAllMiners,
useAllPrs,
useIssues,
useMinersIssues,
useReposAndWeights,
} from '../../api';
import {
type CommitLog,
type DatasetState,
type IssueBounty,
type MinerEvaluation,
type MinerIssue,
type Repository,
} from '../../api/models';
import {
Expand All @@ -28,6 +30,8 @@ import {
buildFeaturedContributors,
buildFeaturedWork,
buildFeaturedDiscoveryContributors,
flattenMinerIssues,
getMirrorSinceParam,
type TrendTimeRange,
} from './dashboardData';

Expand All @@ -36,14 +40,47 @@ type DashboardDatasets = {
miners: DatasetState<MinerEvaluation>;
issues: DatasetState<IssueBounty>;
repos: DatasetState<Repository>;
minerIssues: DatasetState<MinerIssue>;
};

const useDashboardData = (range: TrendTimeRange) => {
const hasIssueActivity = (miner: MinerEvaluation): boolean =>
(miner.totalSolvedIssues ?? 0) +
(miner.totalOpenIssues ?? 0) +
(miner.totalClosedIssues ?? 0) >
0;

export const useDashboardData = (range: TrendTimeRange) => {
const prsQuery = useAllPrs();
const minersQuery = useAllMiners();
const issuesQuery = useIssues();
const reposQuery = useReposAndWeights();

// Fan-out per-miner mirror calls; gate on issue activity to bound parallel requests.
const activeMinerGithubIds = useMemo(
() =>
(minersQuery.data ?? [])
.filter(hasIssueActivity)
.map((m) => m.githubId)
.filter((id): id is string => Boolean(id)),
[minersQuery.data],
);

const minerIssuesSince = getMirrorSinceParam(range);
const minerIssuesQueries = useMinersIssues(
activeMinerGithubIds,
activeMinerGithubIds.length > 0,
minerIssuesSince,
);

const minerIssuesData = useMemo<MinerIssue[]>(
() => flattenMinerIssues(minerIssuesQueries.map((q) => q.data ?? [])),
[minerIssuesQueries],
);
const isMinerIssuesLoading =
activeMinerGithubIds.length > 0 &&
minerIssuesQueries.some((q) => q.isLoading);
const isMinerIssuesError = minerIssuesQueries.some((q) => q.isError);

const datasets: DashboardDatasets = {
prs: {
data: prsQuery.data ?? [],
Expand All @@ -65,6 +102,11 @@ const useDashboardData = (range: TrendTimeRange) => {
isLoading: reposQuery.isLoading,
isError: reposQuery.isError,
},
minerIssues: {
data: minerIssuesData,
isLoading: isMinerIssuesLoading,
isError: isMinerIssuesError,
},
};

const overview = useMemo(
Expand All @@ -75,8 +117,12 @@ const useDashboardData = (range: TrendTimeRange) => {

const trendData = useMemo(
() =>
buildDashboardTrendData(datasets.prs.data, datasets.issues.data, range),
[datasets.issues.data, datasets.prs.data, range],
buildDashboardTrendData(
datasets.prs.data,
datasets.minerIssues.data,
range,
),
[datasets.minerIssues.data, datasets.prs.data, range],
);

const featuredContributors = useMemo(
Expand Down Expand Up @@ -119,11 +165,13 @@ const useDashboardData = (range: TrendTimeRange) => {
isLoading:
datasets.prs.isLoading ||
datasets.miners.isLoading ||
datasets.issues.isLoading,
datasets.issues.isLoading ||
datasets.minerIssues.isLoading,
isError:
datasets.prs.isError ||
datasets.miners.isError ||
datasets.issues.isError,
datasets.issues.isError ||
datasets.minerIssues.isError,
};
};

Expand Down
Loading
Loading