@f0rbit/overview 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/overview +10 -0
- package/dist/overview.js +10361 -0
- package/package.json +22 -15
- package/bunfig.toml +0 -7
- package/packages/core/__tests__/concurrency.test.ts +0 -111
- package/packages/core/__tests__/helpers.ts +0 -60
- package/packages/core/__tests__/integration/git-status.test.ts +0 -62
- package/packages/core/__tests__/integration/scanner.test.ts +0 -140
- package/packages/core/__tests__/ocn.test.ts +0 -164
- package/packages/core/package.json +0 -13
- package/packages/core/src/cache.ts +0 -31
- package/packages/core/src/concurrency.ts +0 -44
- package/packages/core/src/devpad.ts +0 -61
- package/packages/core/src/git-graph.ts +0 -54
- package/packages/core/src/git-stats.ts +0 -201
- package/packages/core/src/git-status.ts +0 -316
- package/packages/core/src/github.ts +0 -286
- package/packages/core/src/index.ts +0 -58
- package/packages/core/src/ocn.ts +0 -74
- package/packages/core/src/scanner.ts +0 -118
- package/packages/core/src/types.ts +0 -199
- package/packages/core/src/watcher.ts +0 -128
- package/packages/core/src/worktree.ts +0 -80
- package/packages/core/tsconfig.json +0 -5
- package/packages/render/bunfig.toml +0 -8
- package/packages/render/jsx-runtime.d.ts +0 -3
- package/packages/render/package.json +0 -18
- package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
- package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
- package/packages/render/src/components/git-graph.tsx +0 -127
- package/packages/render/src/components/help-overlay.tsx +0 -108
- package/packages/render/src/components/index.ts +0 -7
- package/packages/render/src/components/repo-list.tsx +0 -127
- package/packages/render/src/components/stats-panel.tsx +0 -116
- package/packages/render/src/components/status-badge.tsx +0 -70
- package/packages/render/src/components/status-bar.tsx +0 -56
- package/packages/render/src/components/widget-container.tsx +0 -286
- package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
- package/packages/render/src/components/widgets/branch-list.tsx +0 -93
- package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
- package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
- package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
- package/packages/render/src/components/widgets/file-changes.tsx +0 -78
- package/packages/render/src/components/widgets/git-status.tsx +0 -125
- package/packages/render/src/components/widgets/github-ci.tsx +0 -98
- package/packages/render/src/components/widgets/github-issues.tsx +0 -101
- package/packages/render/src/components/widgets/github-prs.tsx +0 -119
- package/packages/render/src/components/widgets/github-release.tsx +0 -73
- package/packages/render/src/components/widgets/index.ts +0 -12
- package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
- package/packages/render/src/components/widgets/registry.ts +0 -23
- package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
- package/packages/render/src/config/index.ts +0 -104
- package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
- package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
- package/packages/render/src/lib/actions.ts +0 -68
- package/packages/render/src/lib/fetch-context.ts +0 -102
- package/packages/render/src/lib/filter.ts +0 -94
- package/packages/render/src/lib/format.ts +0 -36
- package/packages/render/src/lib/use-devpad.ts +0 -167
- package/packages/render/src/lib/use-github.ts +0 -75
- package/packages/render/src/lib/widget-grid.ts +0 -204
- package/packages/render/src/lib/widget-state.ts +0 -96
- package/packages/render/src/overview.tsx +0 -16
- package/packages/render/src/screens/index.ts +0 -1
- package/packages/render/src/screens/main-screen.tsx +0 -410
- package/packages/render/src/theme/index.ts +0 -37
- package/packages/render/tsconfig.json +0 -9
- package/tsconfig.json +0 -23
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { ok, err, type Result } from "@f0rbit/corpus";
|
|
2
|
-
|
|
3
|
-
export type ActionError =
|
|
4
|
-
| { kind: "not_found"; command: string }
|
|
5
|
-
| { kind: "spawn_failed"; command: string; cause: string }
|
|
6
|
-
| { kind: "exited_with_error"; command: string; code: number };
|
|
7
|
-
|
|
8
|
-
type SuspendCallbacks = {
|
|
9
|
-
onSuspend: () => void;
|
|
10
|
-
onResume: () => void;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
async function launchSubprocess(
|
|
14
|
-
command: string,
|
|
15
|
-
args: string[],
|
|
16
|
-
cwd: string,
|
|
17
|
-
callbacks: SuspendCallbacks,
|
|
18
|
-
): Promise<Result<void, ActionError>> {
|
|
19
|
-
callbacks.onSuspend();
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const proc = Bun.spawn([command, ...args], {
|
|
23
|
-
cwd,
|
|
24
|
-
stdin: "inherit",
|
|
25
|
-
stdout: "inherit",
|
|
26
|
-
stderr: "inherit",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
await proc.exited;
|
|
30
|
-
|
|
31
|
-
if (proc.exitCode !== 0 && proc.exitCode !== null) {
|
|
32
|
-
return err({ kind: "exited_with_error", command, code: proc.exitCode });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return ok(undefined);
|
|
36
|
-
} catch (e) {
|
|
37
|
-
return err({ kind: "spawn_failed", command, cause: String(e) });
|
|
38
|
-
} finally {
|
|
39
|
-
callbacks.onResume();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function launchGgi(
|
|
44
|
-
repoPath: string,
|
|
45
|
-
ggiCommand: string,
|
|
46
|
-
callbacks: SuspendCallbacks,
|
|
47
|
-
): Promise<Result<void, ActionError>> {
|
|
48
|
-
return launchSubprocess(ggiCommand, [], repoPath, callbacks);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function launchEditor(
|
|
52
|
-
repoPath: string,
|
|
53
|
-
editorCommand: string,
|
|
54
|
-
callbacks: SuspendCallbacks,
|
|
55
|
-
): Promise<Result<void, ActionError>> {
|
|
56
|
-
const resolved = editorCommand === "$EDITOR"
|
|
57
|
-
? (process.env.EDITOR ?? process.env.VISUAL ?? "vim")
|
|
58
|
-
: editorCommand;
|
|
59
|
-
return launchSubprocess(resolved, ["."], repoPath, callbacks);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function launchSessionizer(
|
|
63
|
-
repoPath: string,
|
|
64
|
-
sessionizerCommand: string,
|
|
65
|
-
callbacks: SuspendCallbacks,
|
|
66
|
-
): Promise<Result<void, ActionError>> {
|
|
67
|
-
return launchSubprocess(sessionizerCommand, [repoPath], process.cwd(), callbacks);
|
|
68
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FetchContext — manages debounced, cancellable, deduplicated async operations.
|
|
3
|
-
*
|
|
4
|
-
* Use for any async work triggered by reactive signal changes where:
|
|
5
|
-
* - Rapid changes should coalesce (debounce)
|
|
6
|
-
* - Stale results should be discarded (cancellation via request ID)
|
|
7
|
-
* - Multiple callers for the same key should share a single in-flight fetch (dedup)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface FetchContext<T> {
|
|
11
|
-
/** Current request ID — increments on each trigger/cancel */
|
|
12
|
-
readonly request_id: number;
|
|
13
|
-
/** Schedule a debounced fetch. Cancels any pending. Returns the new request ID. */
|
|
14
|
-
trigger(fn: () => Promise<T>): number;
|
|
15
|
-
/** Execute immediately, bypassing debounce. Cancels any pending. Returns the new request ID. */
|
|
16
|
-
immediate(fn: () => Promise<T>): number;
|
|
17
|
-
/** Cancel any pending debounce and increment request ID to invalidate in-flight results. */
|
|
18
|
-
cancel(): void;
|
|
19
|
-
/** Clean up timers. Call in onCleanup. */
|
|
20
|
-
dispose(): void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createFetchContext<T>(
|
|
24
|
-
delay_ms: number,
|
|
25
|
-
on_result: (value: T, request_id: number) => void,
|
|
26
|
-
): FetchContext<T> {
|
|
27
|
-
let _request_id = 0;
|
|
28
|
-
let _timer: ReturnType<typeof setTimeout> | undefined;
|
|
29
|
-
|
|
30
|
-
function _run(fn: () => Promise<T>): number {
|
|
31
|
-
const my_id = _request_id;
|
|
32
|
-
fn().then((value) => {
|
|
33
|
-
if (my_id === _request_id) {
|
|
34
|
-
on_result(value, my_id);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
return my_id;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
get request_id() { return _request_id; },
|
|
42
|
-
|
|
43
|
-
trigger(fn) {
|
|
44
|
-
clearTimeout(_timer);
|
|
45
|
-
_request_id++;
|
|
46
|
-
const id = _request_id;
|
|
47
|
-
_timer = setTimeout(() => {
|
|
48
|
-
if (id === _request_id) {
|
|
49
|
-
_run(fn);
|
|
50
|
-
}
|
|
51
|
-
}, delay_ms);
|
|
52
|
-
return _request_id;
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
immediate(fn) {
|
|
56
|
-
clearTimeout(_timer);
|
|
57
|
-
_request_id++;
|
|
58
|
-
return _run(fn);
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
cancel() {
|
|
62
|
-
clearTimeout(_timer);
|
|
63
|
-
_request_id++;
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
dispose() {
|
|
67
|
-
clearTimeout(_timer);
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* InFlightDedup — prevents duplicate concurrent fetches for the same cache key.
|
|
74
|
-
*
|
|
75
|
-
* If a fetch for key K is already in-flight, subsequent callers await the existing
|
|
76
|
-
* promise instead of starting a new one. Once complete, the in-flight entry is removed.
|
|
77
|
-
*/
|
|
78
|
-
export class InFlightDedup<T> {
|
|
79
|
-
private _in_flight = new Map<string, Promise<T>>();
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Run `fn` for `key`, deduplicating against any in-flight fetch for the same key.
|
|
83
|
-
* Returns the result (either from the new fetch or the existing in-flight one).
|
|
84
|
-
*/
|
|
85
|
-
async run(key: string, fn: () => Promise<T>): Promise<T> {
|
|
86
|
-
const existing = this._in_flight.get(key);
|
|
87
|
-
if (existing) return existing;
|
|
88
|
-
|
|
89
|
-
const promise = fn();
|
|
90
|
-
this._in_flight.set(key, promise);
|
|
91
|
-
try {
|
|
92
|
-
return await promise;
|
|
93
|
-
} finally {
|
|
94
|
-
this._in_flight.delete(key);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Check if a fetch is currently in-flight for the given key */
|
|
99
|
-
has(key: string): boolean {
|
|
100
|
-
return this._in_flight.has(key);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { RepoNode, HealthStatus } from "@overview/core";
|
|
2
|
-
|
|
3
|
-
export type SortMode = "name" | "status" | "last-commit";
|
|
4
|
-
export type FilterMode = "all" | "dirty" | "clean" | "ahead" | "behind";
|
|
5
|
-
|
|
6
|
-
const FILTER_MODES: FilterMode[] = ["all", "dirty", "clean", "ahead", "behind"];
|
|
7
|
-
const SORT_MODES: SortMode[] = ["name", "status", "last-commit"];
|
|
8
|
-
|
|
9
|
-
const HEALTH_PRIORITY: Record<HealthStatus, number> = {
|
|
10
|
-
conflict: 0,
|
|
11
|
-
diverged: 1,
|
|
12
|
-
ahead: 2,
|
|
13
|
-
behind: 3,
|
|
14
|
-
dirty: 4,
|
|
15
|
-
clean: 5,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const FILTER_MATCHERS: Record<FilterMode, (h: HealthStatus) => boolean> = {
|
|
19
|
-
all: () => true,
|
|
20
|
-
dirty: (h) => h !== "clean",
|
|
21
|
-
clean: (h) => h === "clean",
|
|
22
|
-
ahead: (h) => h === "ahead" || h === "diverged",
|
|
23
|
-
behind: (h) => h === "behind" || h === "diverged",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function matchesFilter(node: RepoNode, filter: FilterMode): boolean {
|
|
27
|
-
if (!node.status) return false;
|
|
28
|
-
return FILTER_MATCHERS[filter](node.status.health);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function filterTree(nodes: RepoNode[], filter: FilterMode): RepoNode[] {
|
|
32
|
-
if (filter === "all") return nodes;
|
|
33
|
-
return nodes
|
|
34
|
-
.map((node): RepoNode | null => {
|
|
35
|
-
if (node.type === "directory") {
|
|
36
|
-
const children = filterTree(node.children, filter);
|
|
37
|
-
return children.length > 0 ? { ...node, children } : null;
|
|
38
|
-
}
|
|
39
|
-
return matchesFilter(node, filter) ? node : null;
|
|
40
|
-
})
|
|
41
|
-
.filter((n): n is RepoNode => n !== null);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function sortTree(nodes: RepoNode[], sort: SortMode): RepoNode[] {
|
|
45
|
-
const dirs = nodes
|
|
46
|
-
.filter((n) => n.type === "directory")
|
|
47
|
-
.map((n) => ({ ...n, children: sortTree(n.children, sort) }));
|
|
48
|
-
const repos = nodes.filter((n) => n.type !== "directory");
|
|
49
|
-
|
|
50
|
-
const sorted = [...repos].sort((a, b) => {
|
|
51
|
-
switch (sort) {
|
|
52
|
-
case "name":
|
|
53
|
-
return a.name.localeCompare(b.name);
|
|
54
|
-
case "status": {
|
|
55
|
-
const pa = a.status ? HEALTH_PRIORITY[a.status.health] : 6;
|
|
56
|
-
const pb = b.status ? HEALTH_PRIORITY[b.status.health] : 6;
|
|
57
|
-
return pa !== pb ? pa - pb : a.name.localeCompare(b.name);
|
|
58
|
-
}
|
|
59
|
-
case "last-commit": {
|
|
60
|
-
const ta = a.status?.head_time ?? 0;
|
|
61
|
-
const tb = b.status?.head_time ?? 0;
|
|
62
|
-
return tb !== ta ? tb - ta : a.name.localeCompare(b.name);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return [
|
|
68
|
-
...dirs.sort((a, b) => a.name.localeCompare(b.name)),
|
|
69
|
-
...sorted,
|
|
70
|
-
];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function searchRepos(nodes: RepoNode[], query: string): RepoNode[] {
|
|
74
|
-
const q = query.toLowerCase();
|
|
75
|
-
const flatten = (ns: RepoNode[]): RepoNode[] =>
|
|
76
|
-
ns.flatMap((n) =>
|
|
77
|
-
n.type === "directory"
|
|
78
|
-
? flatten(n.children)
|
|
79
|
-
: [n],
|
|
80
|
-
);
|
|
81
|
-
return flatten(nodes).filter((n) => {
|
|
82
|
-
if (n.name.toLowerCase().includes(q)) return true;
|
|
83
|
-
if (n.status?.display_path.toLowerCase().includes(q)) return true;
|
|
84
|
-
return false;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function nextFilter(current: FilterMode): FilterMode {
|
|
89
|
-
return FILTER_MODES[(FILTER_MODES.indexOf(current) + 1) % FILTER_MODES.length]!;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function nextSort(current: SortMode): SortMode {
|
|
93
|
-
return SORT_MODES[(SORT_MODES.indexOf(current) + 1) % SORT_MODES.length]!;
|
|
94
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export function truncate(str: string, maxLen: number): string {
|
|
2
|
-
if (str.length <= maxLen) return str;
|
|
3
|
-
return str.slice(0, maxLen - 1) + "…";
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
|
|
7
|
-
|
|
8
|
-
export function formatBytes(bytes: number): string {
|
|
9
|
-
if (bytes === 0) return "0 B";
|
|
10
|
-
const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
|
|
11
|
-
const value = bytes / 1024 ** exp;
|
|
12
|
-
const unit = BYTE_UNITS[exp]!;
|
|
13
|
-
return value < 10 ? `${value.toFixed(1)} ${unit}` : `${Math.round(value)} ${unit}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function padTo(str: string, len: number): string {
|
|
17
|
-
if (str.length >= len) return str.slice(0, len);
|
|
18
|
-
return str + " ".repeat(len - str.length);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function formatRelativeTime(timestamp: number): string {
|
|
22
|
-
const seconds = Math.floor(Date.now() / 1000) - timestamp;
|
|
23
|
-
if (seconds < 60) return "just now";
|
|
24
|
-
const minutes = Math.floor(seconds / 60);
|
|
25
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
26
|
-
const hours = Math.floor(minutes / 60);
|
|
27
|
-
if (hours < 24) return `${hours}h ago`;
|
|
28
|
-
const days = Math.floor(hours / 24);
|
|
29
|
-
if (days < 7) return `${days}d ago`;
|
|
30
|
-
const weeks = Math.floor(days / 7);
|
|
31
|
-
if (weeks < 5) return `${weeks}w ago`;
|
|
32
|
-
const months = Math.floor(days / 30);
|
|
33
|
-
if (months < 12) return `${months}mo ago`;
|
|
34
|
-
const years = Math.floor(days / 365);
|
|
35
|
-
return `${years}y ago`;
|
|
36
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { createSignal, createEffect, type Accessor } from "solid-js";
|
|
2
|
-
import ApiClient from "@devpad/api";
|
|
3
|
-
import type { Project, TaskWithDetails } from "@devpad/api";
|
|
4
|
-
import {
|
|
5
|
-
DataCache,
|
|
6
|
-
matchRepoToProject,
|
|
7
|
-
type DevpadProject,
|
|
8
|
-
type DevpadTask,
|
|
9
|
-
type DevpadMilestone,
|
|
10
|
-
type DevpadRepoData,
|
|
11
|
-
} from "@overview/core";
|
|
12
|
-
import { InFlightDedup } from "./fetch-context";
|
|
13
|
-
import { getWidgetState } from "./widget-state";
|
|
14
|
-
|
|
15
|
-
type ExtractOkArray<T> = T extends { ok: true; value: (infer U)[] } ? U : T extends { ok: false } ? never : never;
|
|
16
|
-
type ApiMilestone = ExtractOkArray<Awaited<ReturnType<InstanceType<typeof ApiClient>["milestones"]["getByProject"]>>>;
|
|
17
|
-
type ApiGoal = ExtractOkArray<Awaited<ReturnType<InstanceType<typeof ApiClient>["milestones"]["goals"]>>>;
|
|
18
|
-
|
|
19
|
-
const project_cache = new DataCache<DevpadProject[]>();
|
|
20
|
-
const data_cache = new DataCache<DevpadRepoData>();
|
|
21
|
-
const dedup = new InFlightDedup<void>();
|
|
22
|
-
|
|
23
|
-
const PROJECT_CACHE_TTL = 600_000;
|
|
24
|
-
const DATA_CACHE_TTL = 300_000;
|
|
25
|
-
|
|
26
|
-
function toDevpadProject(p: Project): DevpadProject {
|
|
27
|
-
return {
|
|
28
|
-
id: p.id,
|
|
29
|
-
project_id: p.project_id,
|
|
30
|
-
name: p.name,
|
|
31
|
-
description: p.description,
|
|
32
|
-
status: p.status,
|
|
33
|
-
repo_url: p.repo_url,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function toDevpadTask(t: TaskWithDetails): DevpadTask {
|
|
38
|
-
return {
|
|
39
|
-
id: t.task.id,
|
|
40
|
-
title: t.task.title,
|
|
41
|
-
description: t.task.description,
|
|
42
|
-
priority: t.task.priority,
|
|
43
|
-
progress: t.task.progress,
|
|
44
|
-
project_id: t.task.project_id,
|
|
45
|
-
tags: t.tags,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function useDevpad(
|
|
50
|
-
remote_url: Accessor<string | null>,
|
|
51
|
-
repo_name: Accessor<string>,
|
|
52
|
-
): {
|
|
53
|
-
data: Accessor<DevpadRepoData | null>;
|
|
54
|
-
error: Accessor<string | null>;
|
|
55
|
-
loading: Accessor<boolean>;
|
|
56
|
-
} {
|
|
57
|
-
const [data, setData] = createSignal<DevpadRepoData | null>(null);
|
|
58
|
-
const [error, setError] = createSignal<string | null>(null);
|
|
59
|
-
const [loading, setLoading] = createSignal(false);
|
|
60
|
-
|
|
61
|
-
async function fetchData() {
|
|
62
|
-
const state = getWidgetState();
|
|
63
|
-
if (!state.devpad?.api_key) {
|
|
64
|
-
setError("devpad not configured");
|
|
65
|
-
setData(null);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const client = new ApiClient({
|
|
70
|
-
base_url: state.devpad.api_url ?? "https://devpad.tools/api/v1",
|
|
71
|
-
api_key: state.devpad.api_key,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const name = repo_name();
|
|
75
|
-
const url = remote_url();
|
|
76
|
-
|
|
77
|
-
const cache_key = url ?? name;
|
|
78
|
-
const cached = data_cache.get(cache_key);
|
|
79
|
-
if (cached) {
|
|
80
|
-
setData(cached);
|
|
81
|
-
setError(null);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
setLoading(true);
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
await dedup.run(cache_key, async () => {
|
|
89
|
-
let projects = project_cache.get("all");
|
|
90
|
-
if (!projects) {
|
|
91
|
-
const projects_result = await client.projects.list();
|
|
92
|
-
if (!projects_result.ok) {
|
|
93
|
-
throw new Error(projects_result.error.message);
|
|
94
|
-
}
|
|
95
|
-
projects = projects_result.value.map(toDevpadProject);
|
|
96
|
-
project_cache.set("all", projects, PROJECT_CACHE_TTL);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const matched = matchRepoToProject(url, name, projects);
|
|
100
|
-
if (!matched) {
|
|
101
|
-
data_cache.set(cache_key, { project: null, tasks: [], milestones: [] }, DATA_CACHE_TTL);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const [tasks_result, milestones_result] = await Promise.all([
|
|
106
|
-
client.tasks.getByProject(matched.id),
|
|
107
|
-
client.milestones.getByProject(matched.id),
|
|
108
|
-
]);
|
|
109
|
-
|
|
110
|
-
const tasks: DevpadTask[] = tasks_result.ok
|
|
111
|
-
? tasks_result.value
|
|
112
|
-
.map(toDevpadTask)
|
|
113
|
-
.filter((t) => t.progress !== "COMPLETED")
|
|
114
|
-
: [];
|
|
115
|
-
|
|
116
|
-
const raw_milestones: ApiMilestone[] = milestones_result.ok
|
|
117
|
-
? milestones_result.value.filter((m) => !m.finished_at)
|
|
118
|
-
: [];
|
|
119
|
-
|
|
120
|
-
const milestones: DevpadMilestone[] = await Promise.all(
|
|
121
|
-
raw_milestones.map(async (m) => {
|
|
122
|
-
const goals_result = await client.milestones.goals(m.id);
|
|
123
|
-
const goals: ApiGoal[] = goals_result.ok ? goals_result.value : [];
|
|
124
|
-
return {
|
|
125
|
-
id: m.id,
|
|
126
|
-
name: m.name,
|
|
127
|
-
target_version: m.target_version ?? null,
|
|
128
|
-
target_time: m.target_time ?? null,
|
|
129
|
-
finished_at: m.finished_at ?? null,
|
|
130
|
-
goals_total: goals.length,
|
|
131
|
-
goals_completed: goals.filter((g) => g.finished_at !== null).length,
|
|
132
|
-
};
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const repo_data: DevpadRepoData = {
|
|
137
|
-
project: matched,
|
|
138
|
-
tasks,
|
|
139
|
-
milestones,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
data_cache.set(cache_key, repo_data, DATA_CACHE_TTL);
|
|
143
|
-
});
|
|
144
|
-
} catch (e) {
|
|
145
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
146
|
-
setLoading(false);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Read from cache (populated by whichever instance ran first)
|
|
151
|
-
const fresh = data_cache.get(cache_key);
|
|
152
|
-
if (fresh) {
|
|
153
|
-
setData(fresh);
|
|
154
|
-
setError(null);
|
|
155
|
-
}
|
|
156
|
-
setLoading(false);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
createEffect(() => {
|
|
160
|
-
remote_url();
|
|
161
|
-
repo_name();
|
|
162
|
-
getWidgetState();
|
|
163
|
-
fetchData();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
return { data, error, loading };
|
|
167
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { createSignal, createEffect, type Accessor } from "solid-js";
|
|
2
|
-
import { DataCache } from "@overview/core/cache";
|
|
3
|
-
import { collectGithubData, type GithubRepoData, type GithubError } from "@overview/core/github";
|
|
4
|
-
import { InFlightDedup } from "./fetch-context";
|
|
5
|
-
|
|
6
|
-
const cache = new DataCache<GithubRepoData>();
|
|
7
|
-
const dedup = new InFlightDedup<void>();
|
|
8
|
-
|
|
9
|
-
const GITHUB_CACHE_TTL = 120_000;
|
|
10
|
-
|
|
11
|
-
export function useGithub(
|
|
12
|
-
repo_path: Accessor<string | null>,
|
|
13
|
-
remote_url: Accessor<string | null>,
|
|
14
|
-
): {
|
|
15
|
-
data: Accessor<GithubRepoData | null>;
|
|
16
|
-
error: Accessor<GithubError | null>;
|
|
17
|
-
loading: Accessor<boolean>;
|
|
18
|
-
refresh: () => void;
|
|
19
|
-
} {
|
|
20
|
-
const [data, setData] = createSignal<GithubRepoData | null>(null);
|
|
21
|
-
const [error, setError] = createSignal<GithubError | null>(null);
|
|
22
|
-
const [loading, setLoading] = createSignal(false);
|
|
23
|
-
|
|
24
|
-
async function fetchData() {
|
|
25
|
-
const path = repo_path();
|
|
26
|
-
const url = remote_url();
|
|
27
|
-
if (!path) {
|
|
28
|
-
setData(null);
|
|
29
|
-
setError(null);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const cached = cache.get(path);
|
|
34
|
-
if (cached) {
|
|
35
|
-
setData(cached);
|
|
36
|
-
setError(null);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
setLoading(true);
|
|
41
|
-
|
|
42
|
-
// Deduplicate: if another widget instance is already fetching this path,
|
|
43
|
-
// wait for it instead of starting a redundant fetch
|
|
44
|
-
await dedup.run(path, async () => {
|
|
45
|
-
const result = await collectGithubData(path, url);
|
|
46
|
-
if (result.ok) {
|
|
47
|
-
cache.set(path, result.value, GITHUB_CACHE_TTL);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Read result from cache (populated by whichever instance ran first)
|
|
52
|
-
const fresh = cache.get(path);
|
|
53
|
-
if (fresh) {
|
|
54
|
-
setData(fresh);
|
|
55
|
-
setError(null);
|
|
56
|
-
} else {
|
|
57
|
-
setData(null);
|
|
58
|
-
setError(null);
|
|
59
|
-
}
|
|
60
|
-
setLoading(false);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
createEffect(() => {
|
|
64
|
-
repo_path();
|
|
65
|
-
remote_url();
|
|
66
|
-
fetchData();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
data,
|
|
71
|
-
error,
|
|
72
|
-
loading,
|
|
73
|
-
refresh: fetchData,
|
|
74
|
-
};
|
|
75
|
-
}
|