mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Add rebuild cache functionality to issue and patch listing
In case of an outdated cobs cache we display a banner on top of the patch or issue listing, to allow users to rebuild the cobs cache of a repo similar to running `rad patch cache` After rebuilding we fetch the issues or patches from the start. Small caveat with patches: The banner can only be shown after a user fetched all patches from the backend, so we are sure we tried to fetch all of them.
This commit is contained in:
Generated
+1
@@ -4115,6 +4115,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sqlite",
|
||||
"ssh-key",
|
||||
"tauri",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-fs",
|
||||
"tempfile",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use radicle::storage::ReadStorage;
|
||||
|
||||
use radicle::issue::TYPENAME;
|
||||
use radicle_types as types;
|
||||
@@ -66,3 +69,36 @@ pub fn activity_by_issue(
|
||||
) -> Result<Vec<types::cobs::Operation<types::cobs::issue::Action>>, Error> {
|
||||
ctx.activity_by_id(rid, &TYPENAME, id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rebuild_issue_cache(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: identity::RepoId,
|
||||
on_event: tauri::ipc::Channel<types::cobs::CacheEvent>,
|
||||
) -> Result<(), Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let mut issues = ctx.profile.issues_mut(&repo)?;
|
||||
on_event.send(types::cobs::CacheEvent::Started { rid })?;
|
||||
issues.write_all(|result, progress| {
|
||||
match result {
|
||||
Ok((id, _)) => {
|
||||
if on_event
|
||||
.send(types::cobs::CacheEvent::Progress {
|
||||
rid,
|
||||
oid: **id,
|
||||
current: progress.current(),
|
||||
total: progress.total(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send progress");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to retrieve issue: {err}"),
|
||||
};
|
||||
ControlFlow::Continue(())
|
||||
})?;
|
||||
on_event.send(types::cobs::CacheEvent::Finished { rid })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use radicle::patch::TYPENAME;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::{cob, git, identity};
|
||||
|
||||
use radicle_types as types;
|
||||
@@ -125,3 +128,36 @@ pub fn activity_by_patch(
|
||||
) -> Result<Vec<types::cobs::Operation<models::patch::Action>>, Error> {
|
||||
ctx.activity_by_id(rid, &TYPENAME, id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rebuild_patch_cache(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: identity::RepoId,
|
||||
on_event: tauri::ipc::Channel<cobs::CacheEvent>,
|
||||
) -> Result<(), Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let mut patches = ctx.profile.patches_mut(&repo)?;
|
||||
on_event.send(types::cobs::CacheEvent::Started { rid })?;
|
||||
patches.write_all(|result, progress| {
|
||||
match result {
|
||||
Ok((id, _)) => {
|
||||
if on_event
|
||||
.send(cobs::CacheEvent::Progress {
|
||||
rid,
|
||||
oid: **id,
|
||||
current: progress.current(),
|
||||
total: progress.total(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send progress");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to retrieve patch: {err}"),
|
||||
};
|
||||
ControlFlow::Continue(())
|
||||
})?;
|
||||
on_event.send(types::cobs::CacheEvent::Finished { rid })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -31,11 +31,13 @@ pub fn run() {
|
||||
cob::issue::edit_issue,
|
||||
cob::issue::issue_by_id,
|
||||
cob::issue::list_issues,
|
||||
cob::issue::rebuild_issue_cache,
|
||||
cob::patch::activity_by_patch,
|
||||
cob::patch::edit_patch,
|
||||
cob::patch::list_patches,
|
||||
cob::patch::patch_by_id,
|
||||
cob::patch::edit_patch,
|
||||
cob::patch::rebuild_patch_cache,
|
||||
cob::patch::review_by_patch_and_revision_and_id,
|
||||
cob::patch::revisions_by_patch,
|
||||
cob::patch::revision_by_patch_and_id,
|
||||
|
||||
@@ -17,6 +17,7 @@ serde = { version = "1.0.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.0" }
|
||||
sqlite = { version = "0.32.0", features = ["bundled"] }
|
||||
ssh-key = { version = "0.6.3" }
|
||||
tauri = { version = "2.5.0", features = ["isolation"] }
|
||||
tauri-plugin-clipboard-manager = { version = "2.2.2" }
|
||||
tauri-plugin-fs = { version = "2.2.0" }
|
||||
tempfile = { version = "3.19.0" }
|
||||
@@ -39,7 +40,11 @@ tree-sitter-rust = { version = "0.23.2" }
|
||||
tree-sitter-svelte-ng = { version = "1.0.2" }
|
||||
tree-sitter-toml-ng = { version = "0.7.0" }
|
||||
tree-sitter-typescript = { version = "0.23.2" }
|
||||
ts-rs = { version = "10.1.0", features = ["serde-json-impl", "no-serde-warnings", "format"] }
|
||||
ts-rs = { version = "10.1.0", features = [
|
||||
"serde-json-impl",
|
||||
"no-serde-warnings",
|
||||
"format",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
radicle = { version = "0.15.0", features = ["test"] }
|
||||
radicle = { version = "0.15.0", features = ["test"] }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CacheEvent = { "event": "started"; "data": { rid: string } } | {
|
||||
"event": "progress";
|
||||
"data": { rid: string; oid: string; current: number; total: number };
|
||||
} | { "event": "finished"; "data": { rid: string } };
|
||||
@@ -1,3 +1,5 @@
|
||||
use radicle::git::Oid;
|
||||
use radicle::prelude::RepoId;
|
||||
use radicle::profile::Aliases;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
@@ -12,6 +14,29 @@ pub mod repo;
|
||||
pub mod stream;
|
||||
pub mod thread;
|
||||
|
||||
#[derive(ts_rs::TS, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "cob/")]
|
||||
pub enum CacheEvent {
|
||||
Started {
|
||||
#[ts(as = "String")]
|
||||
rid: RepoId,
|
||||
},
|
||||
Progress {
|
||||
#[ts(as = "String")]
|
||||
rid: RepoId,
|
||||
#[ts(as = "String")]
|
||||
oid: Oid,
|
||||
current: usize,
|
||||
total: usize,
|
||||
},
|
||||
Finished {
|
||||
#[ts(as = "String")]
|
||||
rid: RepoId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, TS, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
|
||||
@@ -43,6 +43,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
TauriPluginFs(#[from] tauri_plugin_fs::Error),
|
||||
|
||||
/// Tauri error.
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
|
||||
/// Project error.
|
||||
#[error(transparent)]
|
||||
ProjectError(#[from] radicle::identity::project::ProjectError),
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
variant: "primary" | "secondary" | "ghost" | "float" | "danger" | "success";
|
||||
variant:
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "float"
|
||||
| "danger"
|
||||
| "success"
|
||||
| "outline";
|
||||
hoverable?: boolean;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
stylePosition?: string;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { IssueStatus } from "@app/views/repo/router";
|
||||
import type { ProjectPayload } from "@bindings/repo/ProjectPayload";
|
||||
|
||||
export const issueCounts = $state<
|
||||
Record<IssueStatus, { sidebar: number | null; total: number | null }>
|
||||
>({
|
||||
all: { sidebar: null, total: null },
|
||||
open: { sidebar: null, total: null },
|
||||
closed: { sidebar: null, total: null },
|
||||
});
|
||||
|
||||
export function issueCountMismatch(status: IssueStatus) {
|
||||
return issueCounts[status].total !== issueCounts[status].sidebar;
|
||||
}
|
||||
|
||||
export function resetIssueCounts() {
|
||||
Object.values(issueCounts).forEach(count => {
|
||||
count.sidebar = null;
|
||||
count.total = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateIssueCounts(
|
||||
itemCount: number,
|
||||
sidebarCount: ProjectPayload["meta"]["issues"] & { all: number },
|
||||
status: IssueStatus,
|
||||
) {
|
||||
issueCounts[status].total = itemCount;
|
||||
issueCounts[status].sidebar = sidebarCount[status];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { PatchStatus } from "@app/views/repo/router";
|
||||
import type { ProjectPayload } from "@bindings/repo/ProjectPayload";
|
||||
|
||||
import sum from "lodash/sum";
|
||||
|
||||
export const patchCounts = $state<
|
||||
Record<PatchStatus | "all", { sidebar: number | null; total: number | null }>
|
||||
>({
|
||||
draft: { sidebar: null, total: null },
|
||||
open: { sidebar: null, total: null },
|
||||
archived: { sidebar: null, total: null },
|
||||
merged: { sidebar: null, total: null },
|
||||
all: { sidebar: null, total: null },
|
||||
});
|
||||
|
||||
export function patchCountMismatch(status?: PatchStatus) {
|
||||
return (
|
||||
patchCounts[status || "all"].total !== patchCounts[status || "all"].sidebar
|
||||
);
|
||||
}
|
||||
|
||||
export function resetPatchCounts() {
|
||||
Object.values(patchCounts).forEach(count => {
|
||||
count.sidebar = null;
|
||||
count.total = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePatchCounts(
|
||||
itemCount: number,
|
||||
sidebarCount: ProjectPayload["meta"]["patches"],
|
||||
status?: PatchStatus,
|
||||
) {
|
||||
if (status) {
|
||||
patchCounts[status].total = itemCount;
|
||||
patchCounts[status].sidebar = sidebarCount[status];
|
||||
} else {
|
||||
patchCounts["all"].total = itemCount;
|
||||
patchCounts["all"].sidebar = sum(Object.values(sidebarCount));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { CacheEvent } from "@bindings/cob/CacheEvent";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { Issue } from "@bindings/cob/issue/Issue";
|
||||
import type { IssueStatus } from "./router";
|
||||
import type { IssueStatus } from "@app/views/repo/router";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import delay from "lodash/delay";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import { explorerUrl, modifierKey } from "@app/lib/utils";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import {
|
||||
issueCountMismatch,
|
||||
resetIssueCounts,
|
||||
} from "@app/lib/issueCounts.svelte";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
@@ -15,6 +23,7 @@
|
||||
import IssueTeaser from "@app/components/IssueTeaser.svelte";
|
||||
import IssuesSecondColumn from "@app/components/IssuesSecondColumn.svelte";
|
||||
import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
|
||||
import Spinner from "@app/components/Spinner.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
|
||||
import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
|
||||
@@ -34,10 +43,33 @@
|
||||
let { notificationCount, repo, issues, config, status }: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
let cacheState: CacheEvent | undefined = $state();
|
||||
|
||||
const project = $derived(repo.payloads["xyz.radicle.project"]!);
|
||||
|
||||
let searchInput = $state("");
|
||||
|
||||
async function rebuildIssueCache() {
|
||||
try {
|
||||
await invoke("rebuild_issue_cache", {
|
||||
rid: repo.rid,
|
||||
onEvent: new Channel<CacheEvent>(message => {
|
||||
cacheState = message;
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
issues = await invoke<Issue[]>("list_issues", { rid: repo.rid, status });
|
||||
|
||||
resetIssueCounts();
|
||||
|
||||
delay(() => {
|
||||
cacheState = undefined;
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
status;
|
||||
@@ -112,6 +144,40 @@
|
||||
{/snippet}
|
||||
|
||||
<div class="container">
|
||||
{#if issueCountMismatch(status)}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<Border
|
||||
styleOverflow="hidden"
|
||||
styleBackgroundColor="var(--color-fill-private)"
|
||||
stylePadding="0.25rem 0.5rem"
|
||||
styleGap="1rem"
|
||||
variant="outline">
|
||||
<div class="txt-overflow txt-small global-flex">
|
||||
<Icon name="warning" />
|
||||
<span class="txt-overflow">
|
||||
There’s a problem with your COB cache, so some issues may not be
|
||||
displayed. You can rebuild the cache to resolve this.
|
||||
</span>
|
||||
</div>
|
||||
<div style:margin-left="auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={rebuildIssueCache}
|
||||
disabled={cacheState !== undefined}>
|
||||
{#if cacheState?.event === "started" || cacheState?.event === "progress"}
|
||||
Rebuilding
|
||||
<Spinner />
|
||||
{:else if cacheState?.event === "finished"}
|
||||
Done
|
||||
<Icon name="checkmark" />
|
||||
{:else}
|
||||
Rebuild cache
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Border>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="header">
|
||||
<div>Issues</div>
|
||||
<div class="global-flex" style:margin-left="auto" style:gap="0.75rem">
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type { CacheEvent } from "@bindings/cob/CacheEvent";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { PatchStatus } from "./router";
|
||||
import type { PatchStatus } from "@app/views/repo/router";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import delay from "lodash/delay";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import { DEFAULT_TAKE } from "./router";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { DEFAULT_TAKE } from "@app/views/repo/router";
|
||||
import { explorerUrl, modifierKey } from "@app/lib/utils";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import {
|
||||
patchCountMismatch,
|
||||
resetPatchCounts,
|
||||
updatePatchCounts,
|
||||
} from "@app/lib/patchCounts.svelte";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import NewPatchButton from "@app/components/NewPatchButton.svelte";
|
||||
import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
|
||||
import PatchTeaser from "@app/components/PatchTeaser.svelte";
|
||||
import PatchesSecondColumn from "@app/components/PatchesSecondColumn.svelte";
|
||||
import Spinner from "@app/components/Spinner.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
|
||||
import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
|
||||
@@ -39,10 +49,25 @@
|
||||
let cursor = patches.cursor;
|
||||
let more = patches.more;
|
||||
|
||||
const project = $derived(repo.payloads["xyz.radicle.project"]!);
|
||||
|
||||
let cacheState: CacheEvent | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
items = patches.content;
|
||||
cursor = patches.cursor;
|
||||
more = patches.more;
|
||||
// If the first page is not full, we know there are no more patches.
|
||||
if (patches.more === true && patches.content.length < DEFAULT_TAKE) {
|
||||
more = false;
|
||||
} else {
|
||||
more = patches.more;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (more === false) {
|
||||
updatePatchCounts(items.length, project.meta.patches, status);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -52,6 +77,36 @@
|
||||
searchInput = "";
|
||||
});
|
||||
|
||||
async function rebuildPatchCache() {
|
||||
try {
|
||||
await invoke("rebuild_patch_cache", {
|
||||
rid: repo.rid,
|
||||
onEvent: new Channel<CacheEvent>(message => {
|
||||
cacheState = message;
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
|
||||
rid: repo.rid,
|
||||
skip: 0,
|
||||
status,
|
||||
take: DEFAULT_TAKE,
|
||||
});
|
||||
|
||||
items = p.content;
|
||||
cursor = p.cursor;
|
||||
more = p.more;
|
||||
|
||||
resetPatchCounts();
|
||||
|
||||
delay(() => {
|
||||
cacheState = undefined;
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreContent(all: boolean = false) {
|
||||
if (more) {
|
||||
const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
|
||||
@@ -69,11 +124,18 @@
|
||||
} else {
|
||||
items = [...items, ...p.content];
|
||||
}
|
||||
|
||||
// If the newly fetched patches are empty, there is no more to fetch.
|
||||
if (p.content.length === 0) {
|
||||
more = false;
|
||||
}
|
||||
|
||||
if (more === false) {
|
||||
updatePatchCounts(items.length, project.meta.patches, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const project = $derived(repo.payloads["xyz.radicle.project"]!);
|
||||
|
||||
let loading: boolean = $state(false);
|
||||
let searchInput = $state("");
|
||||
|
||||
@@ -146,6 +208,40 @@
|
||||
{/snippet}
|
||||
|
||||
<div class="container">
|
||||
{#if patchCountMismatch(status)}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<Border
|
||||
styleOverflow="hidden"
|
||||
styleBackgroundColor="var(--color-fill-private)"
|
||||
stylePadding="0.25rem 0.5rem"
|
||||
styleGap="1rem"
|
||||
variant="outline">
|
||||
<div class="txt-overflow txt-small global-flex">
|
||||
<Icon name="warning" />
|
||||
<span class="txt-overflow">
|
||||
There’s a problem with your COB cache, so some patches may not be
|
||||
displayed. You can rebuild the cache to resolve this.
|
||||
</span>
|
||||
</div>
|
||||
<div style:margin-left="auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={rebuildPatchCache}
|
||||
disabled={cacheState !== undefined}>
|
||||
{#if cacheState?.event === "started" || cacheState?.event === "progress"}
|
||||
Rebuilding
|
||||
<Spinner />
|
||||
{:else if cacheState?.event === "finished"}
|
||||
Done
|
||||
<Icon name="checkmark" />
|
||||
{:else}
|
||||
Rebuild cache
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Border>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="header">
|
||||
Patches
|
||||
|
||||
|
||||
Reference in New Issue
Block a user