mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
Allow track and untrack of a project through the TrackButton
- Add support for checking the tracking table from `radicle-httpd` to get a more precise information if the project is being tracked by the local node. - If a user isn't authenticated with their local httpd instance, we still show the Popover to explain how to do it on the CLI
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
Tree,
|
||||
DiffResponse,
|
||||
} from "./lib/project.js";
|
||||
import type { SuccessResponse } from "./lib/shared.js";
|
||||
import type { Comment, Embed } from "./lib/project/comment.js";
|
||||
import type {
|
||||
Commit,
|
||||
@@ -38,6 +39,7 @@ import { z, array, boolean, literal, number, object, string, union } from "zod";
|
||||
import * as project from "./lib/project.js";
|
||||
import * as session from "./lib/session.js";
|
||||
import { Fetcher } from "./lib/fetcher.js";
|
||||
import { successResponseSchema } from "./lib/shared.js";
|
||||
|
||||
export type {
|
||||
BaseUrl,
|
||||
@@ -127,6 +129,16 @@ const nodeInfoSchema = object({
|
||||
),
|
||||
});
|
||||
|
||||
export type NodeTracking = z.infer<typeof nodeTrackingSchema>;
|
||||
|
||||
const nodeTrackingSchema = array(
|
||||
object({
|
||||
id: string(),
|
||||
scope: string(),
|
||||
policy: string(),
|
||||
}),
|
||||
);
|
||||
|
||||
export interface NodeStats {
|
||||
projects: { count: number };
|
||||
users: { count: number };
|
||||
@@ -189,6 +201,49 @@ export class HttpdClient {
|
||||
);
|
||||
}
|
||||
|
||||
public async getTracking(options?: RequestOptions): Promise<NodeTracking> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "GET",
|
||||
path: "node/policies/repos",
|
||||
options,
|
||||
},
|
||||
nodeTrackingSchema,
|
||||
);
|
||||
}
|
||||
|
||||
public async seedById(
|
||||
id: string,
|
||||
authToken: string,
|
||||
options?: RequestOptions,
|
||||
): Promise<SuccessResponse> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `node/policies/repos/${id}`,
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
options,
|
||||
},
|
||||
successResponseSchema,
|
||||
);
|
||||
}
|
||||
|
||||
public async stopSeedingById(
|
||||
id: string,
|
||||
authToken: string,
|
||||
options?: RequestOptions,
|
||||
): Promise<SuccessResponse> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `node/policies/repos/${id}`,
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
options,
|
||||
},
|
||||
successResponseSchema,
|
||||
);
|
||||
}
|
||||
|
||||
public async getNode(options?: RequestOptions): Promise<Node> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import IconSmall from "@app/components/IconSmall.svelte";
|
||||
import Popover from "@app/components/Popover.svelte";
|
||||
|
||||
export let disabled: boolean;
|
||||
export let projectId: string;
|
||||
export let seedCount: number;
|
||||
export let seeding: boolean;
|
||||
|
||||
$: buttonTitle = seeding ? "Seeding" : "Seed";
|
||||
export let editSeeding: (() => Promise<void>) | undefined;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -30,14 +30,21 @@
|
||||
|
||||
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
|
||||
<Button
|
||||
{disabled}
|
||||
slot="toggle"
|
||||
let:toggle
|
||||
on:click={toggle}
|
||||
on:click={async () => {
|
||||
if (editSeeding) {
|
||||
await editSeeding();
|
||||
} else {
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
size="large"
|
||||
variant={seeding ? "secondary-toggle-on" : "secondary-toggle-off"}>
|
||||
<IconSmall name="network" />
|
||||
<span>
|
||||
{buttonTitle}
|
||||
{seeding ? "Seeding" : "Seed"}
|
||||
<span style:font-weight="var(--font-weight-regular)">
|
||||
{seedCount}
|
||||
</span>
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
|
||||
import dompurify from "dompurify";
|
||||
|
||||
import * as modal from "@app/lib/modal";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import markdown from "@app/lib/markdown";
|
||||
import { httpdStore, api } from "@app/lib/httpd";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import CloneButton from "@app/views/projects/Header/CloneButton.svelte";
|
||||
import CopyableId from "@app/components/CopyableId.svelte";
|
||||
import ErrorModal from "@app/modals/ErrorModal.svelte";
|
||||
import Header from "@app/views/projects/Header.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import SeedButton from "@app/views/projects/Header/SeedButton.svelte";
|
||||
@@ -20,8 +23,50 @@
|
||||
export let project: Project;
|
||||
export let seeding: boolean;
|
||||
|
||||
let editSeedingInProgress = false;
|
||||
|
||||
async function editSeeding() {
|
||||
if ($httpdStore.state === "authenticated") {
|
||||
try {
|
||||
editSeedingInProgress = true;
|
||||
if (seeding) {
|
||||
await api.stopSeedingById(project.id, $httpdStore.session.id);
|
||||
} else {
|
||||
await api.seedById(project.id, $httpdStore.session.id);
|
||||
}
|
||||
seeding = !seeding;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
modal.show({
|
||||
component: ErrorModal,
|
||||
props: {
|
||||
title: seeding
|
||||
? "Stop seeding project failed"
|
||||
: "Seeding project failed",
|
||||
subtitle: [
|
||||
`There was an error while trying to ${
|
||||
seeding ? "stop seeding" : "seed"
|
||||
} this project.`,
|
||||
"Check your radicle-httpd logs for details.",
|
||||
],
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
editSeedingInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const render = (content: string): string =>
|
||||
dompurify.sanitize(markdown.parse(content) as string);
|
||||
|
||||
$: session =
|
||||
$httpdStore.state === "authenticated" ? $httpdStore.session : undefined;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -101,8 +146,10 @@
|
||||
style="margin-left: auto; display: flex; gap: 0.5rem;">
|
||||
<SeedButton
|
||||
{seeding}
|
||||
seedCount={project.seeding}
|
||||
projectId={project.id} />
|
||||
disabled={editSeedingInProgress}
|
||||
projectId={project.id}
|
||||
editSeeding={session && editSeeding}
|
||||
seedCount={project.seeding} />
|
||||
<CloneButton {baseUrl} id={project.id} name={project.name} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,12 @@ import type {
|
||||
Tree,
|
||||
} from "@httpd-client";
|
||||
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import * as Syntax from "@app/lib/syntax";
|
||||
import { isLocal, unreachable } from "@app/lib/utils";
|
||||
import { nodePath } from "@app/views/nodes/router";
|
||||
import * as httpd from "@app/lib/httpd";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { ResponseError } from "@httpd-client/lib/fetcher";
|
||||
import { nodePath } from "@app/views/nodes/router";
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
|
||||
export const COMMITS_PER_PAGE = 30;
|
||||
export const PATCHES_PER_PAGE = 10;
|
||||
@@ -245,21 +246,17 @@ function parseRevisionToOid(
|
||||
}
|
||||
|
||||
async function isLocalNodeSeeding(route: ProjectRoute): Promise<boolean> {
|
||||
if (isLocal(route.node.hostname)) {
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
await httpd.api.project.getById(route.project);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
return false;
|
||||
} else {
|
||||
// Either `radicle-httpd` isn't running or there was some other
|
||||
// error.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const tracking = await httpd.api.getTracking();
|
||||
return tracking.some(({ id }) => id === route.project);
|
||||
} catch (error) {
|
||||
if (error instanceof ResponseError && error.status === 404) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Either `radicle-httpd` isn't running or there was some other
|
||||
// error.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
sourceBrowsingRid,
|
||||
test,
|
||||
} from "@tests/support/fixtures.js";
|
||||
import { createProject } from "@tests/support/project";
|
||||
|
||||
test("node metadata", async ({ page, peerManager }) => {
|
||||
const peer = await peerManager.createPeer({
|
||||
@@ -51,3 +52,16 @@ test("node projects", async ({ page }) => {
|
||||
await expect(project.getByText(sourceBrowsingRid)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("seeding projects", async ({ page, authenticatedPeer }) => {
|
||||
const { rid } = await createProject(authenticatedPeer, {
|
||||
name: "seedProject",
|
||||
});
|
||||
|
||||
await page.goto(authenticatedPeer.ridUrl(rid));
|
||||
await page.getByRole("button", { name: "Seeding" }).click();
|
||||
await expect(page.getByRole("button", { name: "Seed" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Seed" }).click();
|
||||
await expect(page.getByRole("button", { name: "Seeding" })).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user