mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Add user page
This commit is contained in:
+40
-1
@@ -97,6 +97,17 @@ const nodeSchema = object({
|
||||
description: string().optional(),
|
||||
});
|
||||
|
||||
export type NodeIdentity = z.infer<typeof nodeIdentitySchema>;
|
||||
|
||||
const nodeIdentitySchema = object({
|
||||
alias: string().nullable(),
|
||||
did: string(),
|
||||
ssh: object({
|
||||
full: string(),
|
||||
hash: string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type NodeInfo = z.infer<typeof nodeInfoSchema>;
|
||||
|
||||
const nodeInfoSchema = object({
|
||||
@@ -202,7 +213,7 @@ export class HttpdClient {
|
||||
);
|
||||
}
|
||||
|
||||
public async getPoliciesById(
|
||||
public async getPolicyById(
|
||||
id: string,
|
||||
options?: RequestOptions,
|
||||
): Promise<SeedingPolicy> {
|
||||
@@ -226,4 +237,32 @@ export class HttpdClient {
|
||||
nodeSchema,
|
||||
);
|
||||
}
|
||||
|
||||
public async getNodeIdentity(
|
||||
id: string,
|
||||
options?: RequestOptions,
|
||||
): Promise<NodeIdentity> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "GET",
|
||||
path: `nodes/${id}`,
|
||||
options,
|
||||
},
|
||||
nodeIdentitySchema,
|
||||
);
|
||||
}
|
||||
|
||||
public async getNodeInventory(
|
||||
id: string,
|
||||
options?: RequestOptions,
|
||||
): Promise<string[]> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "GET",
|
||||
path: `nodes/${id}/inventory`,
|
||||
options,
|
||||
},
|
||||
array(string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +130,14 @@ export class Client {
|
||||
|
||||
public async getByDelegate(
|
||||
delegateId: string,
|
||||
query?: ProjectListQuery,
|
||||
options?: RequestOptions,
|
||||
): Promise<Project[]> {
|
||||
return this.#fetcher.fetchOk(
|
||||
{
|
||||
method: "GET",
|
||||
path: `delegates/${delegateId}/projects`,
|
||||
query,
|
||||
options,
|
||||
},
|
||||
projectsSchema,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import Patch from "@app/views/projects/Patch.svelte";
|
||||
import Patches from "@app/views/projects/Patches.svelte";
|
||||
import Source from "@app/views/projects/Source.svelte";
|
||||
import Users from "@app/views/users/View.svelte";
|
||||
|
||||
import Error from "@app/views/error/View.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
@@ -60,6 +61,8 @@
|
||||
</div>
|
||||
{:else if $activeRouteStore.resource === "nodes"}
|
||||
<Nodes {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "users"}
|
||||
<Users {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "project.source"}
|
||||
<Source {...$activeRouteStore.params} />
|
||||
{:else if $activeRouteStore.resource === "project.history"}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createIcon } from "@app/lib/blockies";
|
||||
|
||||
export let nodeId: string;
|
||||
export let inline = false;
|
||||
export let variant: "small" | "large";
|
||||
|
||||
function createContainer(source: string) {
|
||||
source = source.replace("did:key:", "");
|
||||
@@ -19,20 +19,19 @@
|
||||
<style>
|
||||
.avatar {
|
||||
display: block;
|
||||
border-radius: var(--border-radius-round);
|
||||
box-shadow: 0 0 0 1px var(--color-border-match-background);
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
height: 100%;
|
||||
width: inherit;
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.inline {
|
||||
display: inline-block !important;
|
||||
.small {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: var(--border-radius-round);
|
||||
}
|
||||
.large {
|
||||
width: 4rem;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,5 +39,6 @@
|
||||
title={nodeId}
|
||||
src={createContainer(nodeId)}
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
class:inline />
|
||||
class:small={variant === "small"}
|
||||
class:large={variant === "large"}
|
||||
alt="avatar" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Comment } from "@http-client";
|
||||
import type { BaseUrl, Comment } from "@http-client";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let id: string;
|
||||
export let authorId: string;
|
||||
export let authorAlias: string | undefined = undefined;
|
||||
export let baseUrl: BaseUrl;
|
||||
export let body: string;
|
||||
export let reactions: Comment["reactions"] | undefined = undefined;
|
||||
export let caption = "commented";
|
||||
@@ -107,7 +108,7 @@
|
||||
{/if}
|
||||
<div class="card-header" class:card-header-no-icon={isReply}>
|
||||
<slot class="icon" name="icon" />
|
||||
<NodeId nodeId={authorId} alias={authorAlias} />
|
||||
<NodeId {baseUrl} nodeId={authorId} alias={authorAlias} />
|
||||
<slot name="caption">{caption}</slot>
|
||||
<Id {id} />
|
||||
<span class="timestamp" title={utils.absoluteTimestamp(timestamp)}>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let shorten: boolean = true;
|
||||
export let style: "oid" | "commit" | "none" = "oid";
|
||||
export let ariaLabel: string | undefined = undefined;
|
||||
export let styleWidth: string | undefined = undefined;
|
||||
|
||||
let icon: ComponentProps<Icon>["name"] = "clipboard";
|
||||
const text = "Click to copy";
|
||||
@@ -72,7 +73,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div class="container" style:width={styleWidth}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:mouseenter={() => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl } from "@http-client";
|
||||
import { formatNodeId, parseNodeId, truncateId } from "@app/lib/utils";
|
||||
|
||||
import Avatar from "./Avatar.svelte";
|
||||
import Id from "./Id.svelte";
|
||||
import Link from "./Link.svelte";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let nodeId: string;
|
||||
export let alias: string | undefined = undefined;
|
||||
</script>
|
||||
@@ -12,7 +14,6 @@
|
||||
.avatar-alias {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
height: 1rem;
|
||||
font-family: var(--font-family-monospace);
|
||||
@@ -24,9 +25,9 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<Id id={nodeId} style="none">
|
||||
<div class="avatar-alias">
|
||||
<Avatar {nodeId} />
|
||||
<div class="avatar-alias">
|
||||
<Avatar variant="small" {nodeId} />
|
||||
<Link styleHoverState route={{ resource: "users", did: nodeId, baseUrl }}>
|
||||
{#if alias}
|
||||
<span class="txt-overflow">
|
||||
{alias}
|
||||
@@ -39,5 +40,5 @@
|
||||
{truncateId(parseNodeId(nodeId)?.pubkey || "")}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Id>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
import Link from "@app/components/Link.svelte";
|
||||
|
||||
export let compact = false;
|
||||
|
||||
export let projectInfo: ProjectInfo;
|
||||
|
||||
$: project = projectInfo.project;
|
||||
@@ -155,6 +154,7 @@
|
||||
<Icon name="lock" />
|
||||
</div>
|
||||
{/if}
|
||||
<slot name="delegate" />
|
||||
<Badge
|
||||
variant="neutral"
|
||||
size="tiny"
|
||||
|
||||
@@ -18,9 +18,16 @@ export interface ProjectInfo {
|
||||
export async function fetchProjectInfos(
|
||||
baseUrl: BaseUrl,
|
||||
query?: ProjectListQuery,
|
||||
delegate?: string,
|
||||
): Promise<ProjectInfo[]> {
|
||||
const api = new HttpdClient(baseUrl);
|
||||
const projects = await api.project.getAll(query);
|
||||
let projects: Project[];
|
||||
|
||||
if (delegate) {
|
||||
projects = await api.project.getByDelegate(delegate, query);
|
||||
} else {
|
||||
projects = await api.project.getAll(query);
|
||||
}
|
||||
const info = await Promise.all(
|
||||
projects.map(async project => {
|
||||
const [activity, lastCommit] = await Promise.all([
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Comment } from "@http-client";
|
||||
import type { BaseUrl, Comment } from "@http-client";
|
||||
|
||||
import CommentComponent from "@app/components/Comment.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
@@ -9,6 +9,7 @@
|
||||
replies: Comment[];
|
||||
};
|
||||
export let rawPath: string;
|
||||
export let baseUrl: BaseUrl;
|
||||
|
||||
$: root = thread.root;
|
||||
$: replies = thread.replies;
|
||||
@@ -43,6 +44,7 @@
|
||||
<div class="comments">
|
||||
<div class="top-level-comment" class:has-replies={replies.length > 0}>
|
||||
<CommentComponent
|
||||
{baseUrl}
|
||||
{rawPath}
|
||||
id={root.id}
|
||||
lastEdit={root.edits.length > 1 ? root.edits.pop() : undefined}
|
||||
@@ -58,6 +60,7 @@
|
||||
<div class="replies">
|
||||
{#each replies as reply}
|
||||
<CommentComponent
|
||||
{baseUrl}
|
||||
{rawPath}
|
||||
lastEdit={reply.edits.length > 1 ? reply.edits.pop() : undefined}
|
||||
id={reply.id}
|
||||
|
||||
+12
-1
@@ -13,6 +13,7 @@ import {
|
||||
} from "@app/views/projects/router";
|
||||
import { loadRoute } from "@app/lib/router/definitions";
|
||||
import { nodePath } from "@app/views/nodes/router";
|
||||
import { userRouteToPath, userTitle } from "@app/views/users/router";
|
||||
|
||||
export { type Route };
|
||||
|
||||
@@ -112,6 +113,8 @@ function setTitle(loadedRoute: LoadedRoute) {
|
||||
} else if (loadedRoute.resource === "error") {
|
||||
title.push("Error");
|
||||
title.push("Radicle");
|
||||
} else if (loadedRoute.resource === "users") {
|
||||
title.push(...userTitle(loadedRoute));
|
||||
} else if (loadedRoute.resource === "notFound") {
|
||||
title.push("Page not found");
|
||||
title.push("Radicle");
|
||||
@@ -186,7 +189,13 @@ function urlToRoute(url: URL): Route | null {
|
||||
if (hostAndPort) {
|
||||
const baseUrl = extractBaseUrl(hostAndPort);
|
||||
const id = segments.shift();
|
||||
if (id) {
|
||||
if (id === "users") {
|
||||
const did = segments.shift();
|
||||
if (did) {
|
||||
return { resource: "users", baseUrl, did };
|
||||
}
|
||||
return null;
|
||||
} else if (id) {
|
||||
return resolveProjectRoute(baseUrl, id, segments, url.search);
|
||||
} else {
|
||||
return {
|
||||
@@ -217,6 +226,8 @@ export function routeToPath(route: Route): string {
|
||||
} else {
|
||||
return nodePath(route.params.baseUrl);
|
||||
}
|
||||
} else if (route.resource === "users") {
|
||||
return userRouteToPath(route);
|
||||
} else if (
|
||||
route.resource === "project.source" ||
|
||||
route.resource === "project.history" ||
|
||||
|
||||
@@ -6,11 +6,13 @@ import type {
|
||||
ProjectLoadedRoute,
|
||||
ProjectRoute,
|
||||
} from "@app/views/projects/router";
|
||||
import type { UserLoadedRoute, UserRoute } from "@app/views/users/router";
|
||||
import type { NodesRoute, NodesLoadedRoute } from "@app/views/nodes/router";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type IconLarge from "@app/components/IconLarge.svelte";
|
||||
|
||||
import { loadProjectRoute } from "@app/views/projects/router";
|
||||
import { loadUserRoute } from "@app/views/users/router";
|
||||
import { loadNodeRoute } from "@app/views/nodes/router";
|
||||
|
||||
interface BootingRoute {
|
||||
@@ -36,6 +38,7 @@ export interface ErrorRoute {
|
||||
|
||||
export type Route =
|
||||
| BootingRoute
|
||||
| UserRoute
|
||||
| ErrorRoute
|
||||
| NotFoundRoute
|
||||
| ProjectRoute
|
||||
@@ -43,6 +46,7 @@ export type Route =
|
||||
|
||||
export type LoadedRoute =
|
||||
| BootingRoute
|
||||
| UserLoadedRoute
|
||||
| ErrorRoute
|
||||
| NotFoundRoute
|
||||
| ProjectLoadedRoute
|
||||
@@ -54,6 +58,8 @@ export async function loadRoute(
|
||||
): Promise<LoadedRoute> {
|
||||
if (route.resource === "nodes") {
|
||||
return await loadNodeRoute(route.params);
|
||||
} else if (route.resource === "users") {
|
||||
return await loadUserRoute(route);
|
||||
} else if (
|
||||
route.resource === "project.source" ||
|
||||
route.resource === "project.history" ||
|
||||
|
||||
@@ -89,6 +89,16 @@ export function formatEditedCaption(
|
||||
} edited ${absoluteTimestamp(timestamp)}`;
|
||||
}
|
||||
|
||||
export function formatDid({
|
||||
prefix,
|
||||
pubkey,
|
||||
}: {
|
||||
prefix: string;
|
||||
pubkey: string;
|
||||
}): string {
|
||||
return `${prefix}${pubkey}`;
|
||||
}
|
||||
|
||||
export function baseUrlToString(baseUrl: BaseUrl): string {
|
||||
return `${baseUrl.scheme}://${baseUrl.hostname}:${baseUrl.port}`;
|
||||
}
|
||||
|
||||
@@ -458,7 +458,7 @@
|
||||
styleWidth="100%"
|
||||
let:toggle
|
||||
on:click={toggle}>
|
||||
<Icon name="seedling" />
|
||||
<Icon name="info" />
|
||||
</Button>
|
||||
|
||||
<div slot="popover" style:width="20rem">
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{#each assignees as { id }}
|
||||
<Badge variant="neutral" size="small">
|
||||
<div class="assignee">
|
||||
<Avatar inline nodeId={id} />
|
||||
<Avatar variant="small" nodeId={id} />
|
||||
<span>{formatNodeId(id)}</span>
|
||||
</div>
|
||||
</Badge>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl } from "@http-client";
|
||||
import type { PatchReviews } from "../Patch.svelte";
|
||||
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let reviews: PatchReviews;
|
||||
</script>
|
||||
|
||||
@@ -69,7 +71,10 @@
|
||||
<Icon name="chat" />
|
||||
{/if}
|
||||
</span>
|
||||
<NodeId nodeId={review.author.id} alias={review.author.alias} />
|
||||
<NodeId
|
||||
{baseUrl}
|
||||
nodeId={review.author.id}
|
||||
alias={review.author.alias} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="txt-missing no-reviews">No reviews</div>
|
||||
|
||||
@@ -378,7 +378,10 @@
|
||||
style:padding="0 0.375rem">
|
||||
<Icon name="patch" />
|
||||
</div>
|
||||
<NodeId nodeId={revisionAuthor.id} alias={revisionAuthor.alias} />
|
||||
<NodeId
|
||||
{baseUrl}
|
||||
nodeId={revisionAuthor.id}
|
||||
alias={revisionAuthor.alias} />
|
||||
{#if patchId === revisionId}
|
||||
opened this patch on base
|
||||
<Id id={revisionBase} style="commit" />
|
||||
@@ -455,7 +458,10 @@
|
||||
{#each timelines as element}
|
||||
{#if element.type === "thread"}
|
||||
<div class="connector" />
|
||||
<Thread thread={element.inner} rawPath={rawPath(revisionBase)} />
|
||||
<Thread
|
||||
{baseUrl}
|
||||
thread={element.inner}
|
||||
rawPath={rawPath(revisionBase)} />
|
||||
{:else if element.type === "merge"}
|
||||
<div class="connector" />
|
||||
<div class="action merge">
|
||||
@@ -465,6 +471,7 @@
|
||||
</div>
|
||||
|
||||
<NodeId
|
||||
{baseUrl}
|
||||
nodeId={element.inner.author.id}
|
||||
alias={element.inner.author.alias}>
|
||||
</NodeId>
|
||||
@@ -489,6 +496,7 @@
|
||||
class:positive-review={review.verdict === "accept"}
|
||||
class:negative-review={review.verdict === "reject"}>
|
||||
<CommentComponent
|
||||
{baseUrl}
|
||||
id={review.id}
|
||||
rawPath={rawPath(revisionBase)}
|
||||
authorId={author}
|
||||
|
||||
@@ -181,7 +181,10 @@
|
||||
{issue.state.reason}
|
||||
</Badge>
|
||||
{/if}
|
||||
<NodeId nodeId={issue.author.id} alias={issue.author.alias} />
|
||||
<NodeId
|
||||
{baseUrl}
|
||||
nodeId={issue.author.id}
|
||||
alias={issue.author.alias} />
|
||||
opened
|
||||
<Id id={issue.id} />
|
||||
<span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
|
||||
@@ -228,7 +231,10 @@
|
||||
<div class="connector" />
|
||||
<div class="threads">
|
||||
{#each threads as thread, i (thread.root.id)}
|
||||
<ThreadComponent {thread} rawPath={rawPath(project.head)} />
|
||||
<ThreadComponent
|
||||
{baseUrl}
|
||||
{thread}
|
||||
rawPath={rawPath(project.head)} />
|
||||
{#if i < threads.length - 1}
|
||||
<div class="connector" />
|
||||
{/if}
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
{/if}
|
||||
<div
|
||||
style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<NodeId nodeId={issue.author.id} alias={issue.author.alias} />
|
||||
<NodeId {baseUrl} nodeId={issue.author.id} alias={issue.author.alias} />
|
||||
opened
|
||||
<Id id={issue.id} />
|
||||
<span title={absoluteTimestamp(issue.discussion[0].timestamp)}>
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
|
||||
<Separator />
|
||||
|
||||
<span class="breadcrumb">
|
||||
<span class="breadcrumb" title={project.id}>
|
||||
<Link
|
||||
route={{
|
||||
resource: "project.source",
|
||||
|
||||
@@ -355,7 +355,10 @@
|
||||
insertions={stats.insertions}
|
||||
deletions={stats.deletions} />
|
||||
</Link>
|
||||
<NodeId nodeId={patch.author.id} alias={patch.author.alias} />
|
||||
<NodeId
|
||||
{baseUrl}
|
||||
nodeId={patch.author.id}
|
||||
alias={patch.author.alias} />
|
||||
opened
|
||||
<Id id={patch.id} />
|
||||
<span title={utils.absoluteTimestamp(patch.revisions[0].timestamp)}>
|
||||
@@ -376,7 +379,7 @@
|
||||
<div
|
||||
style:margin-top="2rem"
|
||||
style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<Reviews {reviews} />
|
||||
<Reviews {baseUrl} {reviews} />
|
||||
<Labels labels={patch.labels} />
|
||||
<Embeds embeds={uniqueEmbeds} />
|
||||
</div>
|
||||
@@ -500,7 +503,7 @@
|
||||
</div>
|
||||
|
||||
<div class="metadata global-hide-on-medium-desktop-down">
|
||||
<Reviews {reviews} />
|
||||
<Reviews {baseUrl} {reviews} />
|
||||
<Labels labels={patch.labels} />
|
||||
<Embeds embeds={uniqueEmbeds} />
|
||||
</div>
|
||||
|
||||
@@ -133,7 +133,10 @@
|
||||
{/if}
|
||||
<div
|
||||
style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<NodeId nodeId={patch.author.id} alias={patch.author.alias} />
|
||||
<NodeId
|
||||
{baseUrl}
|
||||
nodeId={patch.author.id}
|
||||
alias={patch.author.alias} />
|
||||
{patch.revisions.length > 1 ? "updated" : "opened"}
|
||||
<Id id={patch.id} />
|
||||
{#if patch.revisions.length > 1}
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
<div class="bottom">
|
||||
<div class="repo box" class:expanded>
|
||||
<ContextRepo
|
||||
{baseUrl}
|
||||
projectThreshold={project.threshold}
|
||||
projectDelegates={project.delegates}
|
||||
{seedingPolicy} />
|
||||
@@ -286,6 +287,7 @@
|
||||
|
||||
<div slot="popover" class="txt-small" style:width="18rem">
|
||||
<ContextRepo
|
||||
{baseUrl}
|
||||
projectThreshold={project.threshold}
|
||||
projectDelegates={project.delegates}
|
||||
{seedingPolicy} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Project, SeedingPolicy } from "@http-client";
|
||||
import type { BaseUrl, Project, SeedingPolicy } from "@http-client";
|
||||
|
||||
import { capitalize } from "lodash";
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let projectThreshold: number;
|
||||
export let projectDelegates: Project["delegates"];
|
||||
export let seedingPolicy: SeedingPolicy;
|
||||
@@ -30,6 +31,7 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.nid {
|
||||
height: 21.5px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -55,11 +57,13 @@
|
||||
changes to be included in the canonical branch.
|
||||
{/if}
|
||||
</div>
|
||||
{#each projectDelegates as delegate}
|
||||
<div class="nid">
|
||||
<NodeId nodeId={delegate.id} alias={delegate.alias} />
|
||||
</div>
|
||||
{/each}
|
||||
<div class="delegates">
|
||||
{#each projectDelegates as delegate}
|
||||
<div class="nid">
|
||||
<NodeId {baseUrl} nodeId={delegate.id} alias={delegate.alias} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="item-header">
|
||||
<span style:text-wrap="nowrap">Seeding Scope</span>
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
{#if selectedPeer}
|
||||
<div class="global-flex-item">
|
||||
<div class="node-id">
|
||||
<Avatar nodeId={selectedPeer.id} inline />
|
||||
<Avatar nodeId={selectedPeer.id} variant="small" />
|
||||
{selectedPeer.alias || formatNodeId(selectedPeer.id)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
<IconButton title="Expand peer" on:click={() => (expanded = !expanded)}>
|
||||
<Icon name={expanded ? "chevron-down" : "chevron-right"} />
|
||||
</IconButton>
|
||||
<NodeId nodeId={peer.remote.id} alias={peer.remote.alias} />
|
||||
<NodeId
|
||||
baseUrl={baseRoute.node}
|
||||
nodeId={peer.remote.id}
|
||||
alias={peer.remote.alias} />
|
||||
{#if peer.remote.delegate}
|
||||
<Badge size="tiny" variant="delegate">
|
||||
<Icon name="badge" />
|
||||
|
||||
@@ -274,7 +274,7 @@ export async function loadProjectRoute(
|
||||
const [project, commit, seedingPolicy, node] = await Promise.all([
|
||||
api.project.getById(route.project),
|
||||
api.project.getCommitBySha(route.project, route.commit),
|
||||
api.getPoliciesById(route.project),
|
||||
api.getPolicyById(route.project),
|
||||
api.getNode(),
|
||||
]);
|
||||
|
||||
@@ -326,7 +326,7 @@ async function loadPatchesView(
|
||||
page: 0,
|
||||
perPage: PATCHES_PER_PAGE,
|
||||
}),
|
||||
api.getPoliciesById(route.project),
|
||||
api.getPolicyById(route.project),
|
||||
api.getNode(),
|
||||
]);
|
||||
|
||||
@@ -356,7 +356,7 @@ async function loadIssuesView(
|
||||
page: 0,
|
||||
perPage: ISSUES_PER_PAGE,
|
||||
}),
|
||||
api.getPoliciesById(route.project),
|
||||
api.getPolicyById(route.project),
|
||||
api.getNode(),
|
||||
]);
|
||||
|
||||
@@ -397,7 +397,7 @@ async function loadTreeView(
|
||||
} else {
|
||||
projectPromise = api.project.getById(route.project);
|
||||
peersPromise = api.project.getAllRemotes(route.project);
|
||||
seedingPolicyPromise = api.getPoliciesById(route.project);
|
||||
seedingPolicyPromise = api.getPolicyById(route.project);
|
||||
}
|
||||
|
||||
const [project, peers, seedingPolicy, node] = await Promise.all([
|
||||
@@ -514,7 +514,7 @@ async function loadHistoryView(
|
||||
const [project, peers, seedingPolicy, branchMap, node] = await Promise.all([
|
||||
api.project.getById(route.project),
|
||||
api.project.getAllRemotes(route.project),
|
||||
api.getPoliciesById(route.project),
|
||||
api.getPolicyById(route.project),
|
||||
getPeerBranches(api, route.project, route.peer),
|
||||
api.getNode(),
|
||||
]);
|
||||
@@ -572,7 +572,7 @@ async function loadIssueView(
|
||||
const [project, issue, seedingPolicy, node] = await Promise.all([
|
||||
api.project.getById(route.project),
|
||||
api.project.getIssueById(route.project, route.issue),
|
||||
api.getPoliciesById(route.project),
|
||||
api.getPolicyById(route.project),
|
||||
api.getNode(),
|
||||
]);
|
||||
return {
|
||||
@@ -600,7 +600,7 @@ async function loadPatchView(
|
||||
const [project, patch, seedingPolicy, node] = await Promise.all([
|
||||
api.project.getById(route.project),
|
||||
api.project.getPatchById(route.project, route.patch),
|
||||
api.getPoliciesById(route.project),
|
||||
api.getPolicyById(route.project),
|
||||
api.getNode(),
|
||||
]);
|
||||
const latestRevision = patch.revisions[patch.revisions.length - 1];
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { formatDid, formatNodeId } from "@app/lib/utils";
|
||||
|
||||
import Id from "@app/components/Id.svelte";
|
||||
|
||||
export let did: { prefix: string; pubkey: string };
|
||||
</script>
|
||||
|
||||
<div style:word-break="break-word">
|
||||
<Id styleWidth="fit-content" ariaLabel="node-id" id={formatDid(did)}>
|
||||
<div class="txt-overflow">
|
||||
{formatNodeId(did.pubkey)}
|
||||
</div>
|
||||
</Id>
|
||||
</div>
|
||||
@@ -0,0 +1,425 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl, NodeIdentity, NodeStats } from "@http-client";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import * as utils from "@app/lib/utils";
|
||||
import { fetchProjectInfos } from "@app/components/ProjectCard";
|
||||
import { handleError } from "@app/views/nodes/error";
|
||||
|
||||
import Avatar from "@app/components/Avatar.svelte";
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Command from "@app/components/Command.svelte";
|
||||
import ExternalLink from "@app/components/ExternalLink.svelte";
|
||||
import Help from "@app/App/Help.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Id from "@app/components/Id.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import MobileFooter from "@app/App/MobileFooter.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import Popover from "@app/components/Popover.svelte";
|
||||
import ProjectCard from "@app/components/ProjectCard.svelte";
|
||||
import Separator from "@app/views/projects/Separator.svelte";
|
||||
import Settings from "@app/App/Settings.svelte";
|
||||
import UserAddress from "@app/views/users/UserAddress.svelte";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let node: NodeIdentity;
|
||||
export let did: { prefix: string; pubkey: string };
|
||||
export let nodeAvatarUrl: string | undefined;
|
||||
export let stats: NodeStats;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-rows: 3.5rem 1fr auto;
|
||||
grid-template-columns: 30rem auto;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar main"
|
||||
"footer main";
|
||||
height: 100vh;
|
||||
}
|
||||
header {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
justify-content: space-between;
|
||||
outline: 1px solid var(--color-fill-separator) !important;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.25rem;
|
||||
line-height: 1rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-small);
|
||||
white-space: nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.breadcrumb :global(a:hover) {
|
||||
color: var(--color-fill-secondary);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
border-right: 1px solid var(--color-fill-separator);
|
||||
width: 30rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 2rem;
|
||||
}
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
.empty-state,
|
||||
.loading {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: scroll;
|
||||
grid-area: main;
|
||||
}
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
max-width: 78rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: var(--button-regular-height);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-footer {
|
||||
display: none;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
border-right: 1px solid var(--color-fill-separator);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-foreground-dim);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.avatar {
|
||||
border-radius: var(--border-radius-tiny);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 30rem) max-content;
|
||||
column-gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.follow-label {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.id {
|
||||
max-width: 22rem;
|
||||
}
|
||||
.repo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (max-width: 1010.98px) {
|
||||
.wrapper {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.sidebar {
|
||||
width: 325px;
|
||||
}
|
||||
.id {
|
||||
max-width: 12rem;
|
||||
}
|
||||
.layout {
|
||||
grid-template-columns: 325px auto;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar main"
|
||||
"footer main";
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 719.98px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"main"
|
||||
"main"
|
||||
"footer";
|
||||
}
|
||||
header {
|
||||
display: none;
|
||||
}
|
||||
.content {
|
||||
overflow-x: hidden;
|
||||
margin-left: 0;
|
||||
}
|
||||
.wrapper {
|
||||
padding: 1rem;
|
||||
}
|
||||
.empty-state,
|
||||
.loading {
|
||||
height: calc(100% - 6rem);
|
||||
}
|
||||
.footer {
|
||||
display: none;
|
||||
}
|
||||
.mobile-footer {
|
||||
grid-area: footer;
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layout">
|
||||
<header>
|
||||
<div class="breadcrumbs">
|
||||
<span class="breadcrumb">
|
||||
<Link
|
||||
style="display: flex; align-items: center; gap: 0.25rem;"
|
||||
route={{
|
||||
resource: "nodes",
|
||||
params: {
|
||||
baseUrl,
|
||||
projectPageIndex: 0,
|
||||
},
|
||||
}}>
|
||||
<img
|
||||
width="24"
|
||||
height="24"
|
||||
class="avatar"
|
||||
alt="Radicle logo"
|
||||
src={nodeAvatarUrl
|
||||
? nodeAvatarUrl
|
||||
: "/images/default-seed-avatar.png"} />
|
||||
{baseUrl.hostname}
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
<Separator />
|
||||
|
||||
<span class="breadcrumb">
|
||||
<Link route={{ resource: "users", did: utils.formatDid(did), baseUrl }}>
|
||||
<div class="breadcrumb" title={utils.formatDid(did)}>
|
||||
{node.alias || utils.formatNodeId(did.pubkey)}
|
||||
</div>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
style="display: flex; align-items: center;"
|
||||
route={{ resource: "nodes", params: undefined }}>
|
||||
<img
|
||||
width="24"
|
||||
height="24"
|
||||
class="logo"
|
||||
alt="Radicle logo"
|
||||
src="/radicle.svg" />
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div class="sidebar global-hide-on-mobile-down">
|
||||
<div class="user-info">
|
||||
<div style:margin-right="0.5rem">
|
||||
<Avatar nodeId={did.pubkey} variant="large" />
|
||||
</div>
|
||||
<div style:margin-top="0.25rem">
|
||||
<div class="txt-medium txt-semibold txt-overflow">
|
||||
{node.alias || utils.formatNodeId(did.pubkey)}
|
||||
</div>
|
||||
<div style:margin-top="0.25rem">
|
||||
<UserAddress {did} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="global-hide-on-small-desktop-down" style="justify-self: end;">
|
||||
<Popover popoverPositionTop="2.5rem">
|
||||
<Button
|
||||
slot="toggle"
|
||||
let:toggle
|
||||
on:click={toggle}
|
||||
variant="secondary">
|
||||
<div class="global-flex-item">
|
||||
<Icon name="plus" />
|
||||
<span>Follow</span>
|
||||
</div>
|
||||
</Button>
|
||||
<div slot="popover" style:width="24rem">
|
||||
<span class="follow-label">
|
||||
Use the <ExternalLink href="https://radicle.xyz">
|
||||
Radicle CLI
|
||||
</ExternalLink> to start following this user.
|
||||
</span>
|
||||
<span class="follow-label">
|
||||
Following a user ensures that their contributions are fetched onto
|
||||
your device.
|
||||
</span>
|
||||
<Command command={`rad follow ${did.pubkey}`} />
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div style:margin-bottom="1rem">
|
||||
<div class="sidebar-item txt-small">
|
||||
<span>SSH Key</span>
|
||||
<Id id={node.ssh.full}>
|
||||
<div class="id txt-overflow">{node.ssh.full}</div>
|
||||
</Id>
|
||||
</div>
|
||||
<div class="sidebar-item txt-small">
|
||||
<span>SSH Hash</span>
|
||||
<Id id={node.ssh.hash}>
|
||||
<div class="id txt-overflow">{node.ssh.hash}</div>
|
||||
</Id>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
|
||||
<Button
|
||||
variant="outline"
|
||||
title="Settings"
|
||||
slot="toggle"
|
||||
let:toggle
|
||||
on:click={toggle}>
|
||||
<Icon name="settings" />
|
||||
Settings
|
||||
</Button>
|
||||
|
||||
<Settings slot="popover" />
|
||||
</Popover>
|
||||
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
|
||||
<Button
|
||||
variant="outline"
|
||||
title="Help"
|
||||
slot="toggle"
|
||||
let:toggle
|
||||
on:click={toggle}>
|
||||
<Icon name="help" />
|
||||
Help
|
||||
</Button>
|
||||
<Help slot="popover" />
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="wrapper">
|
||||
<div class="global-hide-on-small-desktop-up user-info">
|
||||
<div style:margin-right="0.5rem">
|
||||
<Avatar nodeId={did.pubkey} variant="large" />
|
||||
</div>
|
||||
<div style:margin-top="0.25rem">
|
||||
<div class="txt-medium txt-semibold txt-overflow">
|
||||
{node.alias || utils.formatNodeId(did.pubkey)}
|
||||
</div>
|
||||
<div style:margin-top="0.25rem">
|
||||
<UserAddress {did} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#await fetchProjectInfos(baseUrl, { show: "all", perPage: stats.repos.total }, utils.formatDid(did))}
|
||||
<div class="loading">
|
||||
<Loading small center />
|
||||
</div>
|
||||
{:then repos}
|
||||
{#if repos.length > 0}
|
||||
<div class="repo-grid">
|
||||
{#each repos as projectInfo}
|
||||
<ProjectCard {projectInfo}>
|
||||
<svelte:fragment slot="delegate">
|
||||
<Badge
|
||||
title={`${node.alias || utils.formatNodeId(did.pubkey)} is a delegate of this repository`}
|
||||
round
|
||||
variant="delegate"
|
||||
size="tiny"
|
||||
style="padding: 0 0.372rem; gap: 0.125rem;">
|
||||
<Icon name="badge" />
|
||||
</Badge>
|
||||
</svelte:fragment>
|
||||
</ProjectCard>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
{repos.length}
|
||||
{repos.length === 1 ? "repository" : "repositories"}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<Placeholder
|
||||
iconName="desert"
|
||||
caption="This user doesn't have any repositories on this node." />
|
||||
</div>
|
||||
{/if}
|
||||
{:catch error}
|
||||
{router.push(handleError(error, utils.baseUrlToString(baseUrl)))}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-footer">
|
||||
<MobileFooter>
|
||||
<div style:width="100%">
|
||||
<Popover popoverPositionBottom="3rem" popoverPositionRight="-7.5rem">
|
||||
<Button
|
||||
let:expanded
|
||||
slot="toggle"
|
||||
variant={expanded ? "secondary" : "secondary-mobile-toggle"}
|
||||
styleWidth="100%"
|
||||
let:toggle
|
||||
on:click={toggle}>
|
||||
<Icon name="info" />
|
||||
</Button>
|
||||
|
||||
<div slot="popover" style:width="20rem">
|
||||
<div class="sidebar-item txt-small">
|
||||
<span>SSH Key</span>
|
||||
<Id id={node.ssh.full}>
|
||||
<div class="id txt-overflow">{node.ssh.full}</div>
|
||||
</Id>
|
||||
</div>
|
||||
<div class="sidebar-item txt-small">
|
||||
<span>SSH Hash</span>
|
||||
<Id id={node.ssh.hash}>
|
||||
<div class="id txt-overflow">{node.ssh.hash}</div>
|
||||
</Id>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</MobileFooter>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { BaseUrl, NodeIdentity, NodeStats } from "@http-client";
|
||||
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import config from "virtual:config";
|
||||
import { HttpdClient } from "@http-client";
|
||||
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
|
||||
import { handleError } from "@app/views/nodes/error";
|
||||
import { nodePath } from "@app/views/nodes/router";
|
||||
import { unreachableError } from "@app/views/projects/error";
|
||||
|
||||
export interface UserRoute {
|
||||
resource: "users";
|
||||
baseUrl: BaseUrl;
|
||||
did: string;
|
||||
}
|
||||
|
||||
export interface UserLoadedRoute {
|
||||
resource: "users";
|
||||
params: {
|
||||
did: { prefix: string; pubkey: string };
|
||||
baseUrl: BaseUrl;
|
||||
node: NodeIdentity;
|
||||
nodeAvatarUrl: string | undefined;
|
||||
stats: NodeStats;
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadUserRoute({
|
||||
did,
|
||||
baseUrl,
|
||||
}: UserRoute): Promise<UserLoadedRoute | NotFoundRoute | ErrorRoute> {
|
||||
if (
|
||||
import.meta.env.PROD &&
|
||||
utils.isLocal(`${baseUrl.hostname}:${baseUrl.port}`)
|
||||
) {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
icon: "device",
|
||||
title: "Local node browsing not supported",
|
||||
description: `You're trying to access a local node from your browser, we are currently working on a desktop app specific for this use case. Join our <strong>#desktop</strong> channel on <radicle-external-link href="${config.supportWebsite}">${config.supportWebsite}</radicle-external-link> for more information.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const parsedDid = utils.parseNodeId(did);
|
||||
if (!parsedDid) {
|
||||
return {
|
||||
resource: "error",
|
||||
params: {
|
||||
title: "Invalid user DID provided",
|
||||
description:
|
||||
"The provided DID is invalid. Please review the identifier for any errors and try again.",
|
||||
error: new Error(`invalid user DID provided: ${did}`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const api = new HttpdClient(baseUrl);
|
||||
try {
|
||||
const [stats, node, user] = await Promise.all([
|
||||
api.getStats(),
|
||||
api.getNode(),
|
||||
api.getNodeIdentity(parsedDid.pubkey),
|
||||
]);
|
||||
|
||||
return {
|
||||
resource: "users",
|
||||
params: {
|
||||
did: parsedDid,
|
||||
baseUrl,
|
||||
node: user,
|
||||
nodeAvatarUrl: node.avatarUrl,
|
||||
stats,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (
|
||||
error instanceof Error ||
|
||||
error instanceof ResponseError ||
|
||||
error instanceof ResponseParseError
|
||||
) {
|
||||
return handleError(error, utils.baseUrlToString(api.baseUrl));
|
||||
} else {
|
||||
return unreachableError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function userRouteToPath(route: UserRoute): string {
|
||||
return [nodePath(route.baseUrl), "users", route.did].join("/");
|
||||
}
|
||||
|
||||
export function userTitle(route: UserLoadedRoute): string[] {
|
||||
if (route.params.node.alias) {
|
||||
return [route.params.node.alias, utils.formatDid(route.params.did)];
|
||||
}
|
||||
return [utils.formatDid(route.params.did)];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
aliceRemote,
|
||||
bobRemote,
|
||||
} from "@tests/support/fixtures.js";
|
||||
import sinon from "sinon";
|
||||
|
||||
test("user page", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
sinon.useFakeTimers({
|
||||
now: new Date("November 24 2022 12:00:00").valueOf(),
|
||||
shouldClearNativeTimers: true,
|
||||
shouldAdvanceTime: false,
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/nodes/radicle.local/users/${aliceRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("empty pinned projects", async ({ page }) => {
|
||||
await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response parse error", async ({ page }) => {
|
||||
await page.route("*/**/v1/nodes/*", route => {
|
||||
return route.fulfill({
|
||||
json: [{ name: 1337 }],
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response error", async ({ page }) => {
|
||||
await page.route("*/**/v1/nodes/*", route => {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
aliceRemote,
|
||||
bobRemote,
|
||||
} from "@tests/support/fixtures.js";
|
||||
import sinon from "sinon";
|
||||
|
||||
test("user page", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
sinon.useFakeTimers({
|
||||
now: new Date("November 24 2022 12:00:00").valueOf(),
|
||||
shouldClearNativeTimers: true,
|
||||
shouldAdvanceTime: false,
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/nodes/radicle.local/users/${aliceRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("empty pinned projects", async ({ page }) => {
|
||||
await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response parse error", async ({ page }) => {
|
||||
await page.route("*/**/v1/nodes/*", route => {
|
||||
return route.fulfill({
|
||||
json: [{ name: 1337 }],
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("response error", async ({ page }) => {
|
||||
await page.route("*/**/v1/nodes/*", route => {
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(`/nodes/radicle.local/users/${bobRemote}`, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
Reference in New Issue
Block a user