Redesign node view and use it as the landing-page

This commit is contained in:
Rūdolfs Ošiņš
2024-07-26 10:20:01 +02:00
parent 266afb17c8
commit c0049f83ba
50 changed files with 1051 additions and 1021 deletions
+1 -7
View File
@@ -3,13 +3,7 @@
"fallbackPublicExplorer": "FALLBACK_PUBLIC_EXPLORER",
"apiVersion": "API_VERSION",
"defaultHttpdPort": "DEFAULT_HTTPD_PORT",
"defaultHttpdHostname": "DEFAULT_HTTPD_HOSTNAME",
"defaultHttpdScheme": "DEFAULT_HTTPD_SCHEME",
"defaultNodePort": "DEFAULT_NODE_PORT",
"pinned": {
"__name": "PINNED_NODES",
"__format": "json"
}
"defaultHttpdScheme": "DEFAULT_HTTPD_SCHEME"
},
"source": {
"commitsPerPage": "COMMITS_PER_PAGE"
+1 -12
View File
@@ -3,18 +3,7 @@
"fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
"apiVersion": "4.0.0",
"defaultHttpdPort": 443,
"defaultHttpdHostname": "seed.radicle.garden",
"defaultHttpdScheme": "https",
"defaultNodePort": 8776,
"pinned": [
{
"baseUrl": {
"hostname": "seed.radicle.xyz",
"port": 443,
"scheme": "https"
}
}
]
"defaultHttpdScheme": "https"
},
"source": {
"commitsPerPage": 30
+6 -11
View File
@@ -1,16 +1,11 @@
{
"nodes": {
"defaultHttpdPort": 8081,
"defaultHttpdHostname": "127.0.0.1",
"defaultHttpdScheme": "http",
"pinned": [
{
"baseUrl": {
"hostname": "127.0.0.1",
"port": 8081,
"scheme": "http"
}
}
]
"defaultHttpdScheme": "http"
},
"fallbackPreferredSeed": {
"hostname": "127.0.0.1",
"port": 8081,
"scheme": "http"
}
}
Vendored
-3
View File
@@ -4,11 +4,8 @@ declare module "virtual:*" {
apiVersion: string;
fallbackPublicExplorer: string;
defaultHttpdPort: number;
defaultHttpdHostname: string;
defaultLocalHttpdPort: number;
defaultNodePort: number;
defaultHttpdScheme: string;
pinned: { baseUrl: BaseUrl }[];
};
source: {
commitsPerPage: number;
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

+5 -3
View File
@@ -15,7 +15,7 @@ use radicle::Node;
use crate::api::error::Error;
use crate::api::Context;
use crate::axum_extra::Path;
use crate::axum_extra::{cached_response, Path};
pub fn router(ctx: Context) -> Router {
Router::new()
@@ -84,13 +84,15 @@ async fn node_handler(State(ctx): State<Context>) -> impl IntoResponse {
}
};
Ok::<_, Error>(Json(Response::new(
let response = Response::new(
node_id,
agent,
config,
node_state.to_string(),
ctx.profile.config.web.clone(),
)))
);
Ok::<_, Error>(cached_response(response, 600))
}
/// Return stored information about other nodes.
-3
View File
@@ -12,7 +12,6 @@
import Commit from "@app/views/projects/Commit.svelte";
import History from "@app/views/projects/History.svelte";
import Home from "@app/views/home/Index.svelte";
import Issue from "@app/views/projects/Issue.svelte";
import Issues from "@app/views/projects/Issues.svelte";
import Nodes from "@app/views/nodes/View.svelte";
@@ -59,8 +58,6 @@
<div class="loading">
<Loading />
</div>
{:else if $activeRouteStore.resource === "home"}
<Home />
{:else if $activeRouteStore.resource === "nodes"}
<Nodes {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "project.source"}
-34
View File
@@ -1,34 +0,0 @@
<script lang="ts">
import Breadcrumbs from "./Header/Breadcrumbs.svelte";
import Link from "@app/components/Link.svelte";
</script>
<style>
header {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0.5rem 1rem;
height: 3.5rem;
}
.logo {
height: var(--button-regular-height);
margin: 0 0.5rem;
}
</style>
<header>
<Link
style="display: flex; align-items: center;"
route={{ resource: "home" }}>
<img
width="24"
height="24"
class="logo"
alt="Radicle logo"
src="/radicle.svg" />
</Link>
<Breadcrumbs />
</header>
-41
View File
@@ -1,41 +0,0 @@
<script lang="ts">
import * as router from "@app/lib/router";
import * as utils from "@app/lib/utils";
import NodeSegment from "./Breadcrumbs/NodeSegment.svelte";
import ProjectSegment from "./Breadcrumbs/ProjectSegment.svelte";
import Separator from "./Breadcrumbs/Separator.svelte";
const activeRouteStore = router.activeRouteStore;
</script>
<style>
.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;
}
</style>
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "error" || $activeRouteStore.resource === "notFound"}
<!-- Don't render breadcrumbs for these routes. -->
{:else if $activeRouteStore.resource === "nodes"}
<div class="breadcrumbs">
<NodeSegment baseUrl={$activeRouteStore.params.baseUrl} />
</div>
{:else if $activeRouteStore.resource === "project.source" || $activeRouteStore.resource === "project.history" || $activeRouteStore.resource === "project.commit" || $activeRouteStore.resource === "project.issues" || $activeRouteStore.resource === "project.issue" || $activeRouteStore.resource === "project.patches" || $activeRouteStore.resource === "project.patch"}
<div class="breadcrumbs">
<NodeSegment baseUrl={$activeRouteStore.params.baseUrl} />
<Separator />
<ProjectSegment activeRoute={$activeRouteStore} />
</div>
{:else}
{utils.unreachable($activeRouteStore)}
{/if}
@@ -1,29 +0,0 @@
<script lang="ts">
import type { BaseUrl } from "@http-client";
import IconSmall from "@app/components/IconSmall.svelte";
import Link from "@app/components/Link.svelte";
export let baseUrl: BaseUrl;
</script>
<style>
.segment :global(a:hover) {
color: var(--color-fill-secondary);
}
</style>
<span class="segment">
<Link
style="display: flex; align-items: center; gap: 0.25rem;"
route={{
resource: "nodes",
params: {
baseUrl,
projectPageIndex: 0,
},
}}>
<IconSmall name="seedling" />
{baseUrl.hostname}
</Link>
</span>
@@ -1,127 +0,0 @@
<script lang="ts">
import type { ProjectLoadedRoute } from "@app/views/projects/router";
import { formatObjectId, unreachable } from "@app/lib/utils";
import FilePath from "@app/components/FilePath.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Link from "@app/components/Link.svelte";
import Separator from "./Separator.svelte";
export let activeRoute: ProjectLoadedRoute;
</script>
<style>
.segment {
display: flex;
align-items: center;
gap: 0.25rem;
}
.segment :global(a:hover) {
color: var(--color-fill-secondary);
}
.id {
font-size: var(--font-size-small);
font-family: var(--font-family-monospace);
font-weight: var(--font-weight-semibold);
}
</style>
<span class="segment">
<Link
route={{
resource: "project.source",
project: activeRoute.params.project.id,
node: activeRoute.params.baseUrl,
}}>
<div class="segment">
{#if activeRoute.params.project.visibility?.type === "private"}
<IconSmall name="lock" />
{/if}
{activeRoute.params.project.name}
</div>
</Link>
</span>
<span class="segment">
{#if activeRoute.resource === "project.history"}
<Separator />
<Link
route={{
resource: "project.history",
project: activeRoute.params.project.id,
node: activeRoute.params.baseUrl,
}}>
Commits
</Link>
{:else if activeRoute.resource === "project.commit"}
<Separator />
<Link
route={{
resource: "project.history",
project: activeRoute.params.project.id,
node: activeRoute.params.baseUrl,
}}>
Commits
</Link>
{:else if activeRoute.resource === "project.issue" || activeRoute.resource === "project.issues"}
<Separator />
<Link
route={{
resource: "project.issues",
project: activeRoute.params.project.id,
node: activeRoute.params.baseUrl,
}}>
Issues
</Link>
{:else if activeRoute.resource === "project.patch" || activeRoute.resource === "project.patches"}
<Separator />
<Link
route={{
resource: "project.patches",
project: activeRoute.params.project.id,
node: activeRoute.params.baseUrl,
}}>
Patches
</Link>
{:else if activeRoute.resource === "project.source"}
{#if activeRoute.params.path !== "/"}
<Separator />
<FilePath filenameWithPath={activeRoute.params.path} />
{/if}
{:else}
{unreachable(activeRoute)}
{/if}
</span>
{#if activeRoute.resource === "project.commit"}
<Separator />
<span class="id">
<div class="global-hide-on-small-desktop-down">
{activeRoute.params.commit.commit.id}
</div>
<div class="global-hide-on-medium-desktop-up">
{formatObjectId(activeRoute.params.commit.commit.id)}
</div>
</span>
{:else if activeRoute.resource === "project.issue"}
<Separator />
<span class="id">
<div class="global-hide-on-small-desktop-down">
{activeRoute.params.issue.id}
</div>
<div class="global-hide-on-medium-desktop-up">
{formatObjectId(activeRoute.params.issue.id)}
</div>
</span>
{:else if activeRoute.resource === "project.patch"}
<Separator />
<span class="id">
<div class="global-hide-on-small-desktop-down">
{activeRoute.params.patch.id}
</div>
<div class="global-hide-on-medium-desktop-up">
{formatObjectId(activeRoute.params.patch.id)}
</div>
</span>
{/if}
@@ -1,7 +1,7 @@
<script lang="ts">
import Footer from "@app/App/Footer.svelte";
import Header from "@app/App/Header.svelte";
import MobileFooter from "@app/App/MobileFooter.svelte";
import Link from "@app/components/Link.svelte";
</script>
<style>
@@ -12,11 +12,23 @@
}
.header {
border-bottom: 1px solid var(--color-fill-separator);
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0.5rem 0.5rem 0.5rem 1rem;
height: 3.5rem;
justify-content: flex-end;
}
.content {
height: 100%;
overflow-y: scroll;
}
.logo {
height: var(--button-regular-height);
margin: 0 0.5rem;
}
@media (max-width: 719.98px) {
.app {
display: grid;
@@ -31,7 +43,16 @@
<div class="app">
<div class="global-hide-on-mobile-down header">
<Header />
<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>
</div>
<div class="content">
<slot />
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="mobile-footer">
<Link
style="width: 100%; display: flex; align-items: center; justify-content: center;"
route={{ resource: "home" }}>
route={{ resource: "nodes", params: undefined }}>
<img
width="16"
height="16"
+1 -2
View File
@@ -53,7 +53,6 @@
gap: 0.5rem;
justify-content: center;
z-index: 20;
bottom: 1.5rem;
background: var(--color-fill-ghost);
color: var(--color-fill-gray);
border: 1px solid var(--color-border-default);
@@ -101,7 +100,7 @@
</div>
{#if visible}
<div style:position="absolute">
<div style:position="absolute" style:top="-2rem">
<div class="popover">
<IconSmall name={icon} />
{tooltip}
+18 -18
View File
@@ -10,19 +10,31 @@
<style>
.section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
padding-bottom: 0.5rem;
}
.text {
padding-right: 2rem;
}
</style>
<div class="section" style:padding-top="0.5rem">
Policy:
<span class="txt-bold">{capitalize(policy)}</span>
</div>
<div class="txt-missing text">
{#if policy === "allow"}
All discovered repositories will be seeded.
{:else if policy === "block"}
Only selected repositories will be seeded.
{/if}
</div>
{#if policy === "allow"}
<div class="section">
<div class="section" style:padding-top="0.5rem">
Scope:
<span class="txt-bold">{capitalize(scope)}</span>
</div>
<div class="txt-missing">
<div class="txt-missing text">
{#if scope === "all"}
All changes in seeded repositories, made by any peer, will be synced.
{:else if scope === "followed"}
@@ -30,15 +42,3 @@
{/if}
</div>
{/if}
<div class="section">
Policy:
<span class="txt-bold">{capitalize(policy)}</span>
</div>
<div class="txt-missing">
{#if policy === "allow"}
All discovered repositories will get seeded.
{:else if policy === "block"}
Only repositories marked as such will get seeded.
{/if}
</div>
+13 -7
View File
@@ -107,7 +107,7 @@ async function navigate(
function setTitle(loadedRoute: LoadedRoute) {
const title: string[] = [];
if (loadedRoute.resource === "booting" || loadedRoute.resource === "home") {
if (loadedRoute.resource === "booting") {
title.push("Radicle");
} else if (loadedRoute.resource === "error") {
title.push("Error");
@@ -194,11 +194,15 @@ function urlToRoute(url: URL): Route | null {
params: { baseUrl, projectPageIndex: 0 },
};
}
} else {
return {
resource: "nodes",
params: undefined,
};
}
return null;
}
case "": {
return { resource: "home" };
return { resource: "nodes", params: undefined };
}
default: {
return null;
@@ -207,10 +211,12 @@ function urlToRoute(url: URL): Route | null {
}
export function routeToPath(route: Route): string {
if (route.resource === "home") {
return "/";
} else if (route.resource === "nodes") {
return nodePath(route.params.baseUrl);
if (route.resource === "nodes") {
if (route.params === undefined) {
return "/";
} else {
return nodePath(route.params.baseUrl);
}
} else if (
route.resource === "project.source" ||
route.resource === "project.history" ||
-5
View File
@@ -2,7 +2,6 @@ import type {
ResponseError,
ResponseParseError,
} from "@http-client/lib/fetcher";
import type { HomeRoute, HomeLoadedRoute } from "@app/views/home/router";
import type {
ProjectLoadedRoute,
ProjectRoute,
@@ -37,7 +36,6 @@ export interface ErrorRoute {
export type Route =
| BootingRoute
| HomeRoute
| ErrorRoute
| NotFoundRoute
| ProjectRoute
@@ -45,7 +43,6 @@ export type Route =
export type LoadedRoute =
| BootingRoute
| HomeLoadedRoute
| ErrorRoute
| NotFoundRoute
| ProjectLoadedRoute
@@ -57,8 +54,6 @@ export async function loadRoute(
): Promise<LoadedRoute> {
if (route.resource === "nodes") {
return await loadNodeRoute(route.params);
} else if (route.resource === "home") {
return { resource: "home" };
} else if (
route.resource === "project.source" ||
route.resource === "project.history" ||
-5
View File
@@ -8,11 +8,6 @@ export async function toClipboard(text: string): Promise<void> {
await navigator.clipboard.writeText(text);
}
// Removes the first and last character which are always `/`.
export function formatUserAgent(agent: string): string {
return agent.slice(1, -1);
}
export function formatInlineTitle(input: string): string {
return input.replaceAll(/`([^`]+)`/g, "<code>$1</code>");
}
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import AppLayout from "@app/App/AppLayout.svelte";
import Layout from "@app/App/Layout.svelte";
import Icon from "@app/components/Icon.svelte";
export let title: string;
@@ -16,9 +16,9 @@
}
</style>
<AppLayout>
<Layout>
<div class="container">
<Icon name="desert" size="48" />
<div class="title txt-medium txt-bold">{title}</div>
</div>
</AppLayout>
</Layout>
+3 -3
View File
@@ -3,7 +3,7 @@
import type Icon from "@app/components/Icon.svelte";
import type { ErrorParam } from "@app/lib/router/definitions";
import AppLayout from "@app/App/AppLayout.svelte";
import Layout from "@app/App/Layout.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
export let icon: ComponentProps<Icon>["name"] = "desert";
@@ -32,10 +32,10 @@
}
</style>
<AppLayout>
<Layout>
<div class="wrapper">
<div class="container">
<ErrorMessage {icon} {title} {description} {error} />
</div>
</div>
</AppLayout>
</Layout>
-127
View File
@@ -1,127 +0,0 @@
<script lang="ts">
import type { ComponentProps } from "svelte";
import type { ProjectInfo } from "@app/components/ProjectCard";
import { derived } from "svelte/store";
import { baseUrlToString } from "@app/lib/utils";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { fetchProjectInfos } from "@app/components/ProjectCard";
import { handleError } from "@app/views/home/error";
import { preferredSeeds } from "@app/lib/seeds";
import AppLayout from "@app/App/AppLayout.svelte";
import ProjectCard from "@app/components/ProjectCard.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import HomepageSection from "./components/HomepageSection.svelte";
import PreferredSeedDropdown from "./components/PreferredSeedDropdown.svelte";
const selectedSeed = deduplicateStore(
derived(preferredSeeds, $ => $?.selected),
);
let preferredSeedProjects:
| ProjectInfo[]
| ComponentProps<ErrorMessage>["error"]
| undefined;
async function loadPreferredSeedProjects() {
preferredSeedProjects = undefined;
if (!$selectedSeed) return;
preferredSeedProjects = await fetchProjectInfos($selectedSeed, {
show: "pinned",
}).catch(error => error);
}
$: $selectedSeed && void loadPreferredSeedProjects();
</script>
<style>
.wrapper {
padding: 3rem;
max-width: 78rem;
margin: 0 auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 3rem;
}
.empty-state {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.empty-state .heading {
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
}
.empty-state .label {
display: block;
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
}
.flex-icon-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
gap: 1rem;
}
@media (max-width: 719.98px) {
.wrapper {
width: 100%;
padding: 1rem;
}
}
</style>
<AppLayout>
<div class="wrapper" style:padding-bottom="2.5rem">
<HomepageSection
loading={preferredSeedProjects === undefined}
empty={preferredSeedProjects instanceof Error ||
preferredSeedProjects?.length === 0}
title="Explore">
<svelte:fragment slot="title">
<div class="flex-icon-item" style:min-width="0">
<span class="txt-large">Explore</span>
<PreferredSeedDropdown selectedSeed={$preferredSeeds.selected} />
</div>
</svelte:fragment>
<svelte:fragment slot="subtitle">
Pinned repositories on your selected seed node
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="empty-state">
{#if preferredSeedProjects instanceof Error}
<ErrorMessage
{...handleError(
preferredSeedProjects,
baseUrlToString($preferredSeeds.selected),
)} />
{:else}
<div class="heading">No pinned repositories</div>
<div class="label">
The selected seed node doesn't have any pinned repositories.
</div>
{/if}
</div>
</svelte:fragment>
<div class="project-grid">
{#if preferredSeedProjects && !(preferredSeedProjects instanceof Error)}
{#each preferredSeedProjects as projectInfo}
<ProjectCard {projectInfo} />
{/each}
{/if}
</div>
</HomepageSection>
</div>
</AppLayout>
@@ -1,89 +0,0 @@
<script lang="ts">
import Loading from "@app/components/Loading.svelte";
export let title: string;
export let loading = false;
export let empty: boolean = false;
</script>
<style>
.section-header {
margin-bottom: 1.5rem;
}
.title {
width: 100%;
display: flex;
align-items: center;
}
.title > * {
margin: 0;
}
.subtitle {
max-width: 100%;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: center;
margin-top: 0.25rem;
color: var(--color-foreground-dim);
}
.actions {
display: flex;
gap: 0.5rem;
margin-left: auto;
}
.empty-container {
background-color: var(--color-background-float);
border-radius: var(--border-radius-small);
border: 1px solid var(--color-border-hint);
display: flex;
justify-content: center;
padding: 3rem 1rem;
align-items: center;
opacity: 0.75;
}
.empty-container > .inner {
max-width: 36rem;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
<section>
<div class="section-header">
<div class="title">
<slot name="title">
<h2>{title}</h2>
</slot>
<div class="actions">
<slot name="actions" />
</div>
</div>
<div class="subtitle">
<slot name="subtitle" />
</div>
</div>
{#if loading}
<div class="empty-container">
<div class="inner">
<Loading small />
</div>
</div>
{:else if empty}
<div class="empty-container">
<div class="inner">
<slot name="empty" />
</div>
</div>
{:else}
<div>
<slot />
</div>
{/if}
</section>
-40
View File
@@ -1,40 +0,0 @@
import { ResponseParseError, ResponseError } from "@http-client/lib/fetcher";
export function handleError(
error: Error | ResponseParseError | ResponseError,
url: string,
): {
error: Error | ResponseParseError | ResponseError;
title: string;
description: string;
} {
if (error instanceof ResponseParseError) {
return {
error,
title: "Could not parse the request",
description: error.description,
};
} else if (error instanceof ResponseError) {
return {
error,
title: "Could not load the repositories",
description: `You're trying to fetch repositories from a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
};
} else if (
error instanceof TypeError &&
error.message === "Failed to fetch"
) {
return {
error,
title: "Resource not found",
description: `You're trying to fetch a resource that is not available. Check that the ids is correct, and try to run <code>$ rad sync</code>.`,
};
} else {
return {
error,
title: "Could not load this view",
description:
"You stumbled on an unknown error, we aren't exactly sure what happened.",
};
}
}
-5
View File
@@ -1,5 +0,0 @@
export interface HomeRoute {
resource: "home";
}
export interface HomeLoadedRoute extends HomeRoute {}
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import type { Node } from "@http-client";
import { truncateId } from "@app/lib/utils";
import Id from "@app/components/Id.svelte";
export let node: Node;
$: clipboard = node.config?.externalAddresses
? `${node.id}@${node.config.externalAddresses[0]}`
: node.id;
</script>
<div style:word-break="break-word">
<!--prettier-ignore-->
<Id ariaLabel="node-id" shorten={false} id={clipboard} {clipboard}>
{#if node.config?.externalAddresses.length}
{truncateId(node.id)}@<wbr />{node.config?.externalAddresses[0]}
{:else}
{truncateId(node.id)}
{/if}
</Id>
</div>
+55
View File
@@ -0,0 +1,55 @@
<script lang="ts">
import type { DefaultSeedingPolicy } from "@http-client";
import { capitalize } from "lodash";
import IconButton from "@app/components/IconButton.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
export let seedingPolicy: DefaultSeedingPolicy | undefined = undefined;
let expandedNode = false;
$: shortScope =
seedingPolicy?.default === "allow" && seedingPolicy?.scope === "all"
? "permissive"
: "restrictive";
</script>
<style>
.policies {
font-size: var(--font-size-small);
display: flex;
flex-direction: column;
}
.item {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
</style>
<div class="policies">
<div class="item">
<div class="item" style="justify-content: flex-start;">
<span class="no-wrap">Seeding Policy</span>
</div>
<div
style="display: flex; flex-direction: row; gap: 0.5rem; align-items: center;">
<div class="txt-bold">
{capitalize(shortScope)}
</div>
<IconButton on:click={() => (expandedNode = !expandedNode)}>
<IconSmall name={`chevron-${expandedNode ? "up" : "down"}`} />
</IconButton>
</div>
</div>
{#if expandedNode && seedingPolicy}
<div style:padding-bottom="1rem">
<ScopePolicyExplainer {seedingPolicy} />
</div>
{/if}
</div>
@@ -1,15 +1,19 @@
<script lang="ts">
import { HttpdClient, type BaseUrl } from "@http-client";
import { derived } from "svelte/store";
import config from "virtual:config";
import {
addSeedsToConfiguredSeeds,
configuredPreferredSeeds,
preferredSeeds as preferredSeedsStore,
preferredSeeds,
removeSeedFromConfiguredSeeds,
selectPreferredSeed,
} from "@app/lib/seeds";
import { closeFocused } from "@app/components/Popover.svelte";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { push } from "@app/lib/router";
import DropdownList from "@app/components/DropdownList.svelte";
import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
@@ -18,7 +22,9 @@
import Popover from "@app/components/Popover.svelte";
import TextInput from "@app/components/TextInput.svelte";
export let selectedSeed: BaseUrl;
const selectedSeed = deduplicateStore(
derived(preferredSeeds, $ => $?.selected),
);
const validateInput = async (seed: BaseUrl) => {
if (stateOptions.find(s => s.hostname === seed.hostname)) {
@@ -42,7 +48,7 @@
validationMessage = undefined;
valid = true;
}
$: stateOptions = $preferredSeedsStore.seeds;
$: stateOptions = $preferredSeeds.seeds;
let valid = true;
let submittingInput = false;
let validationMessage: undefined | string = undefined;
@@ -75,7 +81,7 @@
.divider {
height: 1px;
width: 100%;
margin: 0.5rem 0.25rem;
margin: 0.5rem 0;
background-color: var(--color-border-default);
}
@@ -90,7 +96,7 @@
bind:expanded
popoverContainerMinWidth="0"
popoverPositionTop="2.5rem"
popoverPositionLeft="-0.25rem"
popoverPositionLeft="0"
popoverPadding="0.25rem"
popoverBorderRadius="var(--border-radius-small)">
<div
@@ -98,7 +104,7 @@
slot="toggle"
title="Switch preferred seeds"
let:toggle>
<div class="txt-large txt-bold txt-overflow">{selectedSeed.hostname}</div>
<slot />
<IconButton on:click={toggle}>
<IconSmall name={expanded ? "chevron-up" : "chevron-down"} />
</IconButton>
@@ -111,7 +117,7 @@
name="seed"
bind:value={customSeed}
loading={submittingInput}
placeholder="Navigate to seed URL"
placeholder="Navigate to seed"
on:submit={async () => {
submittingInput = true;
const customSeedBaseUrl = {
@@ -129,6 +135,10 @@
selectPreferredSeed(customSeedBaseUrl);
customSeed = "";
closeFocused();
void push({
resource: "nodes",
params: { baseUrl: $selectedSeed, projectPageIndex: 0 },
});
} else {
submittingInput = false;
}
@@ -145,9 +155,13 @@
on:click={() => {
selectPreferredSeed(item);
closeFocused();
void push({
resource: "nodes",
params: { baseUrl: $selectedSeed, projectPageIndex: 0 },
});
}}
slot="item"
selected={item.hostname === selectedSeed.hostname}>
selected={item.hostname === $selectedSeed.hostname}>
<div class="dropdown-item">
<div class="icon-item" style:min-width="0">
<IconSmall name="seedling" />
@@ -160,6 +174,11 @@
on:click={() => {
removeSeedFromConfiguredSeeds(item.hostname);
selectPreferredSeed(config.fallbackPreferredSeed);
closeFocused();
void push({
resource: "nodes",
params: { baseUrl: $selectedSeed, projectPageIndex: 0 },
});
}}>
<IconSmall name="cross" />
</IconButton>
@@ -170,6 +189,10 @@
on:click={() => {
selectPreferredSeed(config.fallbackPreferredSeed);
closeFocused();
void push({
resource: "nodes",
params: { baseUrl: $selectedSeed, projectPageIndex: 0 },
});
}}
slot="empty"
selected>
-63
View File
@@ -1,63 +0,0 @@
<script lang="ts">
import type { DefaultSeedingPolicy } from "@http-client";
import capitalize from "lodash/capitalize";
import IconSmall from "@app/components/IconSmall.svelte";
import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
import IconButton from "@app/components/IconButton.svelte";
import Popover from "@app/components/Popover.svelte";
export let seedingPolicy: DefaultSeedingPolicy;
export let popoverPositionRight: string | undefined = undefined;
export let popoverPositionLeft: string | undefined = undefined;
</script>
<style>
.container {
display: flex;
gap: 0.5rem;
align-items: center;
}
.separator {
width: 1px;
background-color: var(--color-fill-separator);
display: flex;
height: 1rem;
}
.popover {
width: 18rem;
color: var(--color-foreground-contrast);
}
</style>
<div class="container">
<span>
Policy: <span class="txt-semibold">
{capitalize(seedingPolicy.default)}
</span>
</span>
{#if seedingPolicy.default === "allow"}
<span class="separator" />
<span>
Scope:
<span class="txt-semibold">{capitalize(seedingPolicy.scope)}</span>
</span>
{/if}
<span style:color="var(--color-fill-gray)">
<Popover
{popoverPositionRight}
{popoverPositionLeft}
popoverPositionBottom="2.5rem">
<IconButton slot="toggle" let:toggle on:click={toggle}>
<span style:color="var(--color-fill-gray)">
<IconSmall name="info" />
</span>
</IconButton>
<div slot="popover" class="popover">
<ScopePolicyExplainer {seedingPolicy} />
</div>
</Popover>
</span>
</div>
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
import IconSmall from "@app/components/IconSmall.svelte";
export let count: number;
const formatter = Intl.NumberFormat("en", { notation: "compact" });
</script>
<style>
.item {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
}
</style>
<div
class="item"
style="justify-content: space-between; display: flex; text-wrap: nowrap; font-size: var(--font-size-small); ">
<span>Seeding</span>
<div class="global-flex-item" style:gap="0.25rem">
<IconSmall name="seedling" />{formatter.format(count).toLowerCase()}
<slot />
</div>
</div>
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
import Id from "@app/components/Id.svelte";
export let agent: string;
</script>
<style>
.agent {
color: var(--color-fill-gray);
font-family: var(--font-family-monospace);
font-size: var(--font-size-small);
max-width: 19rem;
margin-right: 2.25rem;
}
.item {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
font-size: var(--font-size-small);
width: 100%;
}
@media (max-width: 1010.98px) {
.agent {
max-width: 10rem;
}
}
</style>
<div class="item">
<div style:white-space="nowrap">User Agent</div>
<Id
ariaLabel="agent"
id={agent}
clipboard={agent}
shorten={false}
style="none">
<div class="agent">
<div class="txt-overflow">{agent}</div>
</div>
</Id>
</div>
+401 -131
View File
@@ -1,51 +1,117 @@
<script lang="ts">
import type { BaseUrl, DefaultSeedingPolicy, NodeStats } from "@http-client";
import { capitalize } from "lodash";
import type { BaseUrl, Node, NodeStats } from "@http-client";
import * as router from "@app/lib/router";
import { baseUrlToString, formatUserAgent, truncateId } from "@app/lib/utils";
import { baseUrlToString } from "@app/lib/utils";
import { fetchProjectInfos } from "@app/components/ProjectCard";
import { handleError } from "@app/views/nodes/error";
import AppLayout from "@app/App/AppLayout.svelte";
import Settings from "@app/App/Settings.svelte";
import Button from "@app/components/Button.svelte";
import Command from "@app/components/Command.svelte";
import Help from "@app/App/Help.svelte";
import IconButton from "@app/components/IconButton.svelte";
import IconSmall from "@app/components/IconSmall.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 Popover from "@app/components/Popover.svelte";
import ProjectCard from "@app/components/ProjectCard.svelte";
import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
import PolicyExplainer from "./PolicyExplainer.svelte";
import PreferredSeedDropdown from "./PreferredSeedDropdown.svelte";
import Seeding from "./Seeding.svelte";
import UserAgent from "./UserAgent.svelte";
import NodeAddress from "./NodeAddress.svelte";
export let baseUrl: BaseUrl;
export let nid: string;
export let stats: NodeStats;
export let externalAddresses: string[];
export let seedingPolicy: DefaultSeedingPolicy | undefined = undefined;
export let agent: string;
export let node: Node;
$: shortScope =
seedingPolicy?.default === "allow" && seedingPolicy?.scope === "all"
? "permissive"
: "restrictive";
let scrollY: number;
let top: number;
$: if (scrollY >= 0 && scrollY < 289) {
top = 288 - scrollY;
} else if (scrollY >= 289) {
top = 0;
}
$: background = node.bannerUrl
? `url("${node.bannerUrl}")`
: `url("/images/default-seed-header.png")`;
</script>
<style>
.layout {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
padding: 3rem 0 5rem 0;
.below-header {
display: grid;
grid-template: auto 1fr auto / auto 1fr auto;
}
.header {
.breadcrumbs {
display: flex;
gap: 0.5rem;
flex-direction: row;
align-items: center;
height: 3.5rem;
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-small);
padding: 0.5rem 0.5rem 0.5rem 1rem;
justify-content: flex-end;
}
.header {
grid-column: 1 / 4;
border-bottom: 1px solid var(--color-fill-separator);
height: 18rem;
display: flex;
justify-content: space-between;
flex-direction: column;
margin-bottom: 2rem;
position: sticky;
z-index: 5;
background-color: var(--color-background-default);
background-position: center;
background-size: cover;
}
.sidebar {
grid-column: 1 / 2;
border-right: 1px solid var(--color-fill-separator);
width: 30rem;
position: fixed;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1.5rem;
height: 100%;
z-index: 1;
}
.content {
grid-column: 2 / 3;
margin-left: 30rem;
}
.mobile-header {
height: 8rem;
display: flex;
align-items: flex-end;
border-bottom: 1px solid var(--color-fill-separator);
background-color: var(--color-background-default);
background-position: center;
background-size: cover;
}
.mobile-footer {
display: none;
}
.container {
width: 100%;
display: flex;
justify-content: center;
}
.wrapper {
padding: 3rem;
padding: 1.5rem;
max-width: 78rem;
margin: 0 auto;
width: 100%;
@@ -53,148 +119,352 @@
flex-direction: column;
}
.sidebar-item {
display: flex;
align-items: center;
height: 2rem;
}
.subtitle {
font-size: var(--font-size-small);
color: var(--color-foreground-dim);
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
margin-top: 1rem;
}
.info {
display: flex;
justify-content: space-between;
.projects {
margin-top: 0;
}
.agent {
color: var(--color-fill-gray);
font-family: var(--font-family-monospace);
font-size: var(--font-size-small);
}
.seeding-policy {
display: flex;
align-items: center;
gap: 0.5rem;
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
gap: 1rem;
}
.empty-state {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
height: 35vh;
}
.empty-state .heading {
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
}
.empty-state .label {
display: block;
.box {
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
line-height: 1.625rem;
width: 17rem;
}
code {
font-family: var(--font-family-monospace);
font-size: var(--font-size-small);
background-color: var(--color-fill-ghost);
border-radius: var(--border-radius-tiny);
padding: 0.125rem 0.25rem;
}
.desktop-hostname {
max-width: 22rem;
}
@media (max-width: 1010.98px) {
.wrapper {
padding: 1.5rem;
}
.sidebar {
width: 325px;
}
.content {
margin-left: 325px;
}
.desktop-hostname {
max-width: 12rem;
}
}
@media (max-width: 719.98px) {
.title {
display: flex;
flex-direction: column;
margin-left: 1.5rem;
}
.layout {
height: 100%;
}
.below-header {
height: 100%;
}
.header {
display: none;
}
.content {
overflow-x: hidden;
margin-left: 0;
}
.wrapper {
width: 100%;
padding: 1rem;
}
.info {
flex-direction: column;
}
.layout {
.container {
padding: 0;
}
.projects {
margin-top: 3rem;
}
.mobile-footer {
margin-top: auto;
display: grid;
grid-column: 1 / 4;
background-color: pink;
}
}
</style>
<AppLayout>
<div class="layout">
<div class="wrapper">
<div class="header">
<div class="txt-large txt-bold">{baseUrl.hostname}</div>
<div class="info">
<div>
{#each externalAddresses as address}
<!-- If there are externalAddresses this is probably a remote node -->
<!-- in that case, we show all the defined externalAddresses as a listing -->
<Id
ariaLabel="node-id"
shorten={false}
id="{truncateId(nid)}@{address}"
clipboard={`${nid}@${address}`} />
{:else}
<!-- else this is probably a local node -->
<!-- So we show only the nid -->
<div class="global-hide-on-small-desktop-up">
<Id ariaLabel="node-id" id={truncateId(nid)} shorten={false} />
</div>
<div class="global-hide-on-mobile-down">
<Id ariaLabel="node-id" id={nid} shorten={false} />
</div>
{/each}
</div>
<Id
ariaLabel="agent"
id={formatUserAgent(agent)}
shorten={false}
style="none">
<div class="agent">
{formatUserAgent(agent)}
</div>
</Id>
</div>
</div>
<svelte:window bind:scrollY />
<div class="subtitle" style:justify-content="space-between">
<div class="txt-semibold">Pinned repositories</div>
<div class="seeding-policy">
{#if seedingPolicy}
<span class="txt-bold">Seeding Policy:</span>
{capitalize(shortScope)}
<div class="global-hide-on-mobile-down">
<Popover
popoverPositionBottom="0"
popoverPositionLeft="-17rem"
popoverPositionRight="2rem">
<IconButton slot="toggle" let:toggle on:click={toggle}>
<IconSmall name="help" />
</IconButton>
<ScopePolicyExplainer slot="popover" {seedingPolicy} />
</Popover>
</div>
{/if}
<div class="layout">
<div class="header" style:background-image={background}>
<div class="breadcrumbs">
<Link
style="display: flex; align-items: center;"
route={{ resource: "nodes", params: undefined }}>
<div
style="background-color: var(--color-background-default);border-radius: var(--border-radius-small); display: flex; padding: 0.5rem 0;">
<img
style:margin="0 0.5rem"
width="24"
height="24"
class="logo"
alt="Radicle logo"
src="/radicle.svg" />
</div>
</div>
<div style:margin-top="1rem" style:padding-bottom="2.5rem">
{#await fetchProjectInfos( baseUrl, { show: "pinned", perPage: stats.repos.total }, )}
<div style:height="35vh">
<Loading small center />
</div>
{:then projectInfos}
{#if projectInfos.length > 0}
<div class="project-grid">
{#each projectInfos as projectInfo}
<ProjectCard {projectInfo} />
{/each}
</div>
{:else}
<div class="empty-state">
<div class="heading">No pinned repositories</div>
<div class="label">
This node doesn't have any pinned repositories.
</div>
</div>
{/if}
{:catch error}
{router.push(handleError(error, baseUrlToString(baseUrl)))}
{/await}
</div>
</Link>
</div>
</div>
</AppLayout>
<div class="below-header">
<div
class="sidebar global-hide-on-mobile-down"
style:top={`${top}px`}
style:height={`calc(100% - ${top}px)`}>
<div class="title">
<div
style="display: flex; align-items: center; gap: 1rem;"
style:margin-bottom="1.5rem">
<img
style:border-radius="var(--border-radius-small)"
style:min-width="64px"
width="64"
height="64"
class="avatar"
alt="Seed avatar"
src={node.avatarUrl
? node.avatarUrl
: "/images/default-seed-avatar.png"} />
<div>
<div class="global-flex-item desktop-hostname">
<PreferredSeedDropdown>
<div class="txt-medium txt-semibold txt-overflow">
{baseUrl.hostname}
</div>
</PreferredSeedDropdown>
</div>
<NodeAddress {node} />
</div>
</div>
{#if node.description}
<div class="description txt-small">
{node.description}
</div>
{:else}
<div
class="global-flex-item txt-small txt-missing"
style:align-items="center"
style:justify-content="space-between"
style:gap="0.25rem">
No description configured.
<Popover popoverPositionTop="0" popoverPositionLeft="2.25rem">
<IconButton slot="toggle" let:toggle on:click={toggle}>
<IconSmall name="info" />
</IconButton>
<div slot="popover" class="box">
If you're the owner of this node, you can customize this page by
setting the
<code>avatarUrl</code>
,
<code>bannerUrl</code>
and
<code>description</code>
fields under the
<code>web</code>
object in your node config.
<div style:margin-top="1rem">
<Command command="rad config edit" fullWidth />
</div>
</div>
</Popover>
</div>
{/if}
<div
style:display="flex"
style:margin-top="1.5rem"
style:margin-bottom="1rem"
style:flex-direction="column">
<PolicyExplainer seedingPolicy={node.config?.seedingPolicy} />
<div class="sidebar-item">
<Seeding count={stats.repos.total}>
<div style:width="2rem" />
</Seeding>
</div>
<div class="sidebar-item">
<UserAgent agent={node.agent} />
</div>
</div>
</div>
<div class="sidebar-footer">
<div
style:margin-top="1.5rem"
style:display="flex"
style:justify-content="space-between"
style:flex-direction="row">
<div class="horizontal-buttons">
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="outline"
title="Settings"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="settings" />
Settings
</Button>
<Settings slot="popover" />
</Popover>
</div>
<div class="horizontal-buttons">
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="outline"
title="Help"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="help" />
Help
</Button>
<Help slot="popover" />
</Popover>
</div>
</div>
</div>
</div>
<div class="content">
<div class="global-hide-on-small-desktop-up">
<div
class="mobile-header txt-huge txt-semibold"
style:background-image={background}>
</div>
</div>
<div class="container">
<div class="wrapper">
<div
class="global-hide-on-small-desktop-up"
style="display: flex; align-items: center; gap: 1rem;">
<img
style:min-width="64px"
style:border-radius="var(--border-radius-small)"
width="64"
height="64"
alt="Seed avatar"
src={node.avatarUrl
? node.avatarUrl
: "/images/default-seed-avatar.png"} />
<div>
<div class="global-flex-item">
<PreferredSeedDropdown>
<div class="txt-medium txt-semibold txt-overflow">
{baseUrl.hostname}
</div>
</PreferredSeedDropdown>
</div>
<NodeAddress {node} />
</div>
</div>
{#if node.description}
<div
class="global-hide-on-small-desktop-up"
style:margin-top="1.5rem"
style:display="flex"
style:flex-direction="column"
style:gap="0.25rem">
{#if node.description}
<div class="description txt-small">
{node.description}
</div>
{/if}
</div>
{:else}
<div
class="global-flex-item txt-small txt-missing global-hide-on-small-desktop-up"
style:margin-top="1.5rem">
No description configured.
</div>
{/if}
<div class="projects">
{#await fetchProjectInfos( baseUrl, { show: "pinned", perPage: stats.repos.total }, )}
<div style:height="35vh">
<Loading small center />
</div>
{:then projectInfos}
{#if projectInfos.length > 0}
<div class="project-grid">
{#each projectInfos as projectInfo}
<ProjectCard {projectInfo} />
{/each}
</div>
<div class="subtitle">
{projectInfos.length}
pinned {projectInfos.length === 1
? "repository"
: "repositories"}
</div>
{:else}
<div class="empty-state">
This node doesn't have any pinned repositories.
</div>
{/if}
{:catch error}
{router.push(handleError(error, baseUrlToString(baseUrl)))}
{/await}
</div>
</div>
</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}>
<IconSmall name="seedling" />
</Button>
<div slot="popover" style:width="20rem">
<PolicyExplainer seedingPolicy={node.config?.seedingPolicy} />
<UserAgent agent={node.agent} />
</div>
</Popover>
</div>
</MobileFooter>
</div>
</div>
</div>
+24 -19
View File
@@ -1,4 +1,4 @@
import type { BaseUrl, DefaultSeedingPolicy, NodeStats } from "@http-client";
import type { BaseUrl, Node, NodeStats } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import config from "virtual:config";
@@ -7,11 +7,15 @@ import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { baseUrlToString, isLocal } from "@app/lib/utils";
import { handleError } from "@app/views/nodes/error";
import { unreachableError } from "@app/views/projects/error";
import { preferredSeeds } from "@app/lib/seeds";
import { get } from "svelte/store";
export interface NodesRouteParams {
baseUrl: BaseUrl;
projectPageIndex: number;
}
export type NodesRouteParams =
| {
baseUrl: BaseUrl;
projectPageIndex: number;
}
| undefined;
export interface NodesRoute {
resource: "nodes";
@@ -22,11 +26,8 @@ export interface NodesLoadedRoute {
resource: "nodes";
params: {
baseUrl: BaseUrl;
agent: string;
externalAddresses: string[];
nid: string;
stats: NodeStats;
seedingPolicy?: DefaultSeedingPolicy;
node: Node;
};
}
@@ -43,10 +44,17 @@ export function nodePath(baseUrl: BaseUrl) {
export async function loadNodeRoute(
params: NodesRouteParams,
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
if (
import.meta.env.PROD &&
isLocal(`${params.baseUrl.hostname}:${params.baseUrl.port}`)
) {
let baseUrl: BaseUrl;
if (params) {
baseUrl = params.baseUrl;
} else {
baseUrl = get(preferredSeeds).selected;
}
const api = new HttpdClient(baseUrl);
if (import.meta.env.PROD && isLocal(`${baseUrl.hostname}:${baseUrl.port}`)) {
return {
resource: "error",
params: {
@@ -56,19 +64,16 @@ export async function loadNodeRoute(
},
};
}
const api = new HttpdClient(params.baseUrl);
try {
const [node, stats] = await Promise.all([api.getNode(), api.getStats()]);
return {
resource: "nodes",
params: {
baseUrl: params.baseUrl,
nid: node.id,
baseUrl,
node,
stats,
externalAddresses: node.config?.externalAddresses ?? [],
seedingPolicy: node.config?.seedingPolicy,
agent: node.agent,
},
};
} catch (error) {
+26 -2
View File
@@ -1,20 +1,24 @@
<script lang="ts">
import type { BaseUrl, Commit, Project, SeedingPolicy } from "@http-client";
import { formatObjectId } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import Changeset from "@app/views/projects/Changeset.svelte";
import CommitAuthorship from "@app/views/projects/Commit/CommitAuthorship.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Id from "@app/components/Id.svelte";
import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
import Layout from "./Layout.svelte";
import Link from "@app/components/Link.svelte";
import Separator from "./Separator.svelte";
import Share from "./Share.svelte";
import Id from "@app/components/Id.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
export let commit: Commit;
export let project: Project;
export let nodeAvatarUrl: string | undefined;
$: header = commit.commit;
</script>
@@ -45,7 +49,27 @@
}
</style>
<Layout {seedingPolicy} {baseUrl} {project}>
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {project}>
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
route={{
resource: "project.history",
project: project.id,
node: baseUrl,
}}>
Commits
</Link>
<Separator />
<span class="id">
<div class="global-hide-on-small-desktop-down">
{commit.commit.id}
</div>
<div class="global-hide-on-medium-desktop-up">
{formatObjectId(commit.commit.id)}
</div>
</span>
</svelte:fragment>
<div class="commit">
<div class="header">
<div style="display:flex; flex-direction: column; gap: 0.5rem;">
+16 -2
View File
@@ -4,8 +4,8 @@
CommitHeader,
Project,
Remote,
Tree,
SeedingPolicy,
Tree,
} from "@http-client";
import type { ProjectRoute } from "./router";
@@ -19,9 +19,11 @@
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import Header from "./Source/Header.svelte";
import Layout from "./Layout.svelte";
import Link from "@app/components/Link.svelte";
import List from "@app/components/List.svelte";
import Loading from "@app/components/Loading.svelte";
import ProjectNameHeader from "./Source/ProjectNameHeader.svelte";
import Separator from "./Separator.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
@@ -32,6 +34,7 @@
export let project: Project;
export let revision: string | undefined;
export let tree: Tree;
export let nodeAvatarUrl: string | undefined;
const api = new HttpdClient(baseUrl);
@@ -89,7 +92,18 @@
}
</style>
<Layout {seedingPolicy} {baseUrl} {project} activeTab="source">
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {project} activeTab="source">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
route={{
resource: "project.history",
project: project.id,
node: baseUrl,
}}>
Commits
</Link>
</svelte:fragment>
<ProjectNameHeader {project} {baseUrl} slot="header" />
<div style:margin="1rem" slot="subheader">
+31 -1
View File
@@ -15,9 +15,11 @@
import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
import Labels from "@app/views/projects/Cob/Labels.svelte";
import Layout from "./Layout.svelte";
import Link from "@app/components/Link.svelte";
import Markdown from "@app/components/Markdown.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Reactions from "@app/components/Reactions.svelte";
import Separator from "./Separator.svelte";
import Share from "@app/views/projects/Share.svelte";
import ThreadComponent from "@app/components/Thread.svelte";
@@ -26,6 +28,7 @@
export let issue: Issue;
export let project: Project;
export let rawPath: (commit?: string) => string;
export let nodeAvatarUrl: string | undefined;
$: uniqueEmbeds = uniqBy(
issue.discussion.flatMap(comment => comment.embeds),
@@ -110,6 +113,11 @@
align-items: center;
margin-left: -0.25rem;
}
.id {
font-size: var(--font-size-small);
font-family: var(--font-family-monospace);
font-weight: var(--font-weight-semibold);
}
@media (max-width: 719.98px) {
.bottom {
padding: 0;
@@ -118,11 +126,33 @@
</style>
<Layout
{seedingPolicy}
{baseUrl}
{nodeAvatarUrl}
{project}
{seedingPolicy}
activeTab="issues"
stylePaddingBottom="0">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
route={{
resource: "project.issues",
project: project.id,
node: baseUrl,
}}>
Issues
</Link>
<Separator />
<span class="id">
<div class="global-hide-on-small-desktop-down">
{issue.id}
</div>
<div class="global-hide-on-medium-desktop-up">
{utils.formatObjectId(issue.id)}
</div>
</span>
</svelte:fragment>
<div class="issue">
<div class="main">
<CobHeader>
+14 -1
View File
@@ -25,6 +25,7 @@
import Loading from "@app/components/Loading.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Popover from "@app/components/Popover.svelte";
import Separator from "./Separator.svelte";
import Share from "./Share.svelte";
export let baseUrl: BaseUrl;
@@ -32,6 +33,7 @@
export let issues: Issue[];
export let project: Project;
export let status: IssueState["status"];
export let nodeAvatarUrl: string | undefined;
let loading = false;
let page = 0;
@@ -115,7 +117,18 @@
}
</style>
<Layout {seedingPolicy} {baseUrl} {project} activeTab="issues">
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {project} activeTab="issues">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
route={{
resource: "project.issues",
project: project.id,
node: baseUrl,
}}>
Issues
</Link>
</svelte:fragment>
<div slot="header" class="header">
<Popover
popoverPadding="0"
+94 -3
View File
@@ -2,12 +2,11 @@
import type { ActiveTab } from "./Header.svelte";
import type { BaseUrl, Project, SeedingPolicy } from "@http-client";
import AppHeader from "@app/App/Header.svelte";
import Button from "@app/components/Button.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Link from "@app/components/Link.svelte";
import MobileFooter from "@app/App/MobileFooter.svelte";
import Separator from "./Separator.svelte";
import Sidebar from "@app/views/projects/Sidebar.svelte";
export let activeTab: ActiveTab | undefined = undefined;
@@ -15,6 +14,7 @@
export let baseUrl: BaseUrl;
export let project: Project;
export let stylePaddingBottom: string = "2.5rem";
export let nodeAvatarUrl: string | undefined;
</script>
<style>
@@ -29,6 +29,21 @@
border-bottom: 1px solid var(--color-fill-separator);
}
header {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0.5rem 0.5rem 0.5rem 1rem;
height: 3.5rem;
justify-content: space-between;
}
.logo {
height: var(--button-regular-height);
margin: 0 0.5rem;
}
.sidebar {
grid-column: 1 / 2;
border-right: 1px solid var(--color-fill-separator);
@@ -43,6 +58,29 @@
display: none;
}
.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);
}
.avatar {
border-radius: var(--border-radius-tiny);
margin-right: 0.5rem;
}
@media (max-width: 719.98px) {
.desktop-header {
display: none;
@@ -62,7 +100,60 @@
<div class="layout">
<div class="desktop-header">
<AppHeader />
<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: "project.source",
project: project.id,
node: baseUrl,
}}>
<div class="breadcrumb">
{project.name}
</div>
</Link>
</span>
<div class="breadcrumb">
<slot name="breadcrumb" />
</div>
</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>
<div class="sidebar global-hide-on-medium-desktop-down">
+29 -1
View File
@@ -71,6 +71,7 @@
import Reviews from "@app/views/projects/Cob/Reviews.svelte";
import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
import RevisionSelector from "@app/views/projects/Patch/RevisionSelector.svelte";
import Separator from "./Separator.svelte";
import Share from "@app/views/projects/Share.svelte";
export let baseUrl: BaseUrl;
@@ -80,6 +81,7 @@
export let rawPath: (commit?: string) => string;
export let project: Project;
export let view: PatchView;
export let nodeAvatarUrl: string | undefined;
function badgeColor(status: string): ComponentProps<Badge>["variant"] {
if (status === "draft") {
@@ -278,6 +280,11 @@
gap: 0.5rem;
width: 100%;
}
.id {
font-size: var(--font-size-small);
font-family: var(--font-family-monospace);
font-weight: var(--font-weight-semibold);
}
@media (max-width: 719.98px) {
.patch {
display: block;
@@ -289,11 +296,32 @@
</style>
<Layout
{seedingPolicy}
{baseUrl}
{project}
{nodeAvatarUrl}
{seedingPolicy}
activeTab="patches"
stylePaddingBottom="0">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
route={{
resource: "project.patches",
project: project.id,
node: baseUrl,
}}>
Patches
</Link>
<Separator />
<span class="id">
<div class="global-hide-on-small-desktop-down">
{patch.id}
</div>
<div class="global-hide-on-medium-desktop-up">
{utils.formatObjectId(patch.id)}
</div>
</span>
</svelte:fragment>
<div class="patch">
<div class="main">
<CobHeader>
+14 -1
View File
@@ -25,6 +25,7 @@
import PatchTeaser from "./Patch/PatchTeaser.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Popover, { closeFocused } from "@app/components/Popover.svelte";
import Separator from "./Separator.svelte";
import Share from "./Share.svelte";
export let baseUrl: BaseUrl;
@@ -32,6 +33,7 @@
export let patches: Patch[];
export let project: Project;
export let status: PatchState["status"];
export let nodeAvatarUrl: string | undefined;
let loading = false;
let page = 0;
@@ -123,7 +125,18 @@
}
</style>
<Layout {seedingPolicy} {baseUrl} {project} activeTab="patches">
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {project} activeTab="patches">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
route={{
resource: "project.patches",
project: project.id,
node: baseUrl,
}}>
Patches
</Link>
</svelte:fragment>
<div slot="header" class="header">
<Popover
popoverPadding="0"
+29 -35
View File
@@ -139,13 +139,6 @@
opacity: 1;
transition: opacity 150ms ease-in-out;
}
.collapse-label {
display: none;
}
.collapse-label.expanded {
display: block;
transition: opacity 30ms ease-in-out;
}
.icon {
transform: rotate(180deg);
transition: transform 150ms ease-in-out;
@@ -301,36 +294,37 @@
<div class="icon" class:expanded>
<IconSmall name="chevron-left" />
</div>
<span class="collapse-label" class:expanded>Collapse</span>
</Button>
<div class="horizontal-buttons" class:expanded>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="background"
title="Settings"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="settings" />
Settings
</Button>
<div class="global-flex-item">
<div class="horizontal-buttons" class:expanded>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="outline"
title="Settings"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="settings" />
Settings
</Button>
<Settings slot="popover" />
</Popover>
</div>
<div class="horizontal-buttons" class:expanded>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="background"
title="Help"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="help" />
Help
</Button>
<Help slot="popover" />
</Popover>
<Settings slot="popover" />
</Popover>
</div>
<div class="horizontal-buttons" class:expanded>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="outline"
title="Help"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="help" />
Help
</Button>
<Help slot="popover" />
</Popover>
</div>
</div>
</div>
{/if}
+13 -3
View File
@@ -16,8 +16,10 @@
import Placeholder from "@app/components/Placeholder.svelte";
import BlobComponent from "./Source/Blob.svelte";
import TreeComponent from "./Source/Tree.svelte";
import FilePath from "@app/components/FilePath.svelte";
import ProjectNameHeader from "./Source/ProjectNameHeader.svelte";
import Separator from "./Separator.svelte";
import TreeComponent from "./Source/Tree.svelte";
export let baseUrl: BaseUrl;
export let blobResult: BlobResult;
@@ -26,10 +28,11 @@
export let peer: string | undefined;
export let peers: Remote[];
export let project: Project;
export let seedingPolicy: SeedingPolicy;
export let rawPath: (commit?: string) => string;
export let revision: string | undefined;
export let seedingPolicy: SeedingPolicy;
export let tree: Tree;
export let nodeAvatarUrl: string | undefined;
let mobileFileTree = false;
@@ -117,11 +120,18 @@
</style>
<Layout
{seedingPolicy}
{baseUrl}
{nodeAvatarUrl}
{project}
{seedingPolicy}
activeTab="source"
stylePaddingBottom="0">
<svelte:fragment slot="breadcrumb">
{#if path !== "/"}
<Separator />
<FilePath filenameWithPath={path} />
{/if}
</svelte:fragment>
<ProjectNameHeader {project} {baseUrl} slot="header" />
<div style:margin="1rem" slot="subheader">
+28 -7
View File
@@ -120,6 +120,7 @@ export type ProjectLoadedRoute =
path: string;
rawPath: (commit?: string) => string;
blobResult: BlobResult;
nodeAvatarUrl: string | undefined;
};
}
| {
@@ -134,6 +135,7 @@ export type ProjectLoadedRoute =
revision: string | undefined;
tree: Tree;
commitHeaders: CommitHeader[];
nodeAvatarUrl: string | undefined;
};
}
| {
@@ -143,6 +145,7 @@ export type ProjectLoadedRoute =
seedingPolicy: SeedingPolicy;
project: Project;
commit: Commit;
nodeAvatarUrl: string | undefined;
};
}
| {
@@ -153,6 +156,7 @@ export type ProjectLoadedRoute =
project: Project;
rawPath: (commit?: string) => string;
issue: Issue;
nodeAvatarUrl: string | undefined;
};
}
| {
@@ -163,6 +167,7 @@ export type ProjectLoadedRoute =
project: Project;
issues: Issue[];
status: IssueState["status"];
nodeAvatarUrl: string | undefined;
};
}
| {
@@ -173,6 +178,7 @@ export type ProjectLoadedRoute =
project: Project;
patches: Patch[];
status: PatchState["status"];
nodeAvatarUrl: string | undefined;
};
}
| {
@@ -185,6 +191,7 @@ export type ProjectLoadedRoute =
patch: Patch;
stats: Diff["stats"];
view: PatchView;
nodeAvatarUrl: string | undefined;
};
};
@@ -264,10 +271,11 @@ export async function loadProjectRoute(
} else if (route.resource === "project.history") {
return await loadHistoryView(route);
} else if (route.resource === "project.commit") {
const [project, commit, seedingPolicy] = await Promise.all([
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.getNode(),
]);
return {
@@ -277,6 +285,7 @@ export async function loadProjectRoute(
seedingPolicy,
project,
commit,
nodeAvatarUrl: node.avatarUrl,
},
};
} else if (route.resource === "project.issue") {
@@ -310,7 +319,7 @@ async function loadPatchesView(
const searchParams = new URLSearchParams(route.search || "");
const status = (searchParams.get("status") as PatchState["status"]) || "open";
const [project, patches, seedingPolicy] = await Promise.all([
const [project, patches, seedingPolicy, node] = await Promise.all([
api.project.getById(route.project),
api.project.getAllPatches(route.project, {
status,
@@ -318,6 +327,7 @@ async function loadPatchesView(
perPage: PATCHES_PER_PAGE,
}),
api.getPoliciesById(route.project),
api.getNode(),
]);
return {
@@ -328,6 +338,7 @@ async function loadPatchesView(
patches,
status,
project,
nodeAvatarUrl: node.avatarUrl,
},
};
}
@@ -338,7 +349,7 @@ async function loadIssuesView(
const api = new HttpdClient(route.node);
const status = route.status || "open";
const [project, issues, seedingPolicy] = await Promise.all([
const [project, issues, seedingPolicy, node] = await Promise.all([
api.project.getById(route.project),
api.project.getAllIssues(route.project, {
status,
@@ -346,6 +357,7 @@ async function loadIssuesView(
perPage: ISSUES_PER_PAGE,
}),
api.getPoliciesById(route.project),
api.getNode(),
]);
return {
@@ -356,6 +368,7 @@ async function loadIssuesView(
issues,
status,
project,
nodeAvatarUrl: node.avatarUrl,
},
};
}
@@ -387,10 +400,11 @@ async function loadTreeView(
seedingPolicyPromise = api.getPoliciesById(route.project);
}
const [project, peers, seedingPolicy] = await Promise.all([
const [project, peers, seedingPolicy, node] = await Promise.all([
projectPromise,
peersPromise,
seedingPolicyPromise,
api.getNode(),
]);
let branchMap: Record<string, string> = {
@@ -438,6 +452,7 @@ async function loadTreeView(
tree,
path,
blobResult,
nodeAvatarUrl: node.avatarUrl,
},
};
}
@@ -496,11 +511,12 @@ async function loadHistoryView(
): Promise<ProjectLoadedRoute> {
const api = new HttpdClient(route.node);
const [project, peers, seedingPolicy, branchMap] = await Promise.all([
const [project, peers, seedingPolicy, branchMap, node] = await Promise.all([
api.project.getById(route.project),
api.project.getAllRemotes(route.project),
api.getPoliciesById(route.project),
getPeerBranches(api, route.project, route.peer),
api.getNode(),
]);
let commitId;
@@ -539,6 +555,7 @@ async function loadHistoryView(
revision: route.revision,
tree,
commitHeaders,
nodeAvatarUrl: node.avatarUrl,
},
};
}
@@ -552,10 +569,11 @@ async function loadIssueView(
route.project
}${commit ? `/${commit}` : ""}`;
const [project, issue, seedingPolicy] = await Promise.all([
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.getNode(),
]);
return {
resource: "project.issue",
@@ -565,6 +583,7 @@ async function loadIssueView(
project,
rawPath,
issue,
nodeAvatarUrl: node.avatarUrl,
},
};
}
@@ -578,10 +597,11 @@ async function loadPatchView(
route.project
}${commit ? `/${commit}` : ""}`;
const [project, patch, seedingPolicy] = await Promise.all([
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.getNode(),
]);
const latestRevision = patch.revisions[patch.revisions.length - 1];
const { diff } = await api.project.getDiff(
@@ -643,6 +663,7 @@ async function loadPatchView(
patch,
stats: diff.stats,
view,
nodeAvatarUrl: node.avatarUrl,
},
};
}
+4 -2
View File
@@ -1,9 +1,11 @@
import { test, expect } from "@tests/support/fixtures.js";
test("exceptions in production build", async ({ page }) => {
await page.goto("/");
await page.goto("/nodes/127.0.0.1:8081");
// Wait for scripts to finish executing, there might be exceptions that
// happen after the page has been painted.
await page.waitForTimeout(2000);
await expect(page.getByText("Explore", { exact: true })).toBeVisible();
await expect(
page.getByText("Local node browsing not supported"),
).toBeVisible();
});
-8
View File
@@ -4,7 +4,6 @@ import {
expect,
sourceBrowsingUrl,
sourceBrowsingRid,
nodeRemote,
test,
} from "@tests/support/fixtures.js";
@@ -60,13 +59,6 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
);
}
await page.goto("/nodes/radicle.local");
// Node address.
{
await page.getByRole("button", { name: "node-id" }).first().click();
await expectClipboard(`${nodeRemote}`, page);
}
// Clear the system clipboard contents so developers don't wonder why there's
// random stuff in their clipboard after running tests.
await page.evaluate<string>("navigator.clipboard.writeText('')");
-143
View File
@@ -1,143 +0,0 @@
import { expect, test } from "@tests/support/fixtures.js";
test("show pinned repositories", async ({ context, page }) => {
await context.addInitScript(() => {
localStorage.setItem(
"configuredPreferredSeeds",
JSON.stringify([{ hostname: "127.0.0.1", port: 8081, scheme: "http" }]),
);
});
await page.goto("/");
// Shows pinned project name.
await expect(page.getByText("source-browsing")).toBeVisible();
//
// Shows pinned project description.
await expect(
page.getByText("Git repository for source browsing tests"),
).toBeVisible();
});
test("no duplicate entry for preferred seeds", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("seed.radicle.garden")).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.radicle.garden" }),
).toBeVisible();
await page
.getByPlaceholder("Navigate to seed URL")
.fill("seed.radicle.garden");
await page.getByPlaceholder("Navigate to seed URL").press("Enter");
await expect(page.getByText("Seed node already added.")).toBeVisible();
await page.getByPlaceholder("Navigate to seed URL").fill("");
await expect(page.getByText("Seed node already added.")).toBeHidden();
});
test("adding and removing a new preferred seed", async ({ page }) => {
await page.route(
({ hostname }) => hostname === "seed.rhizoma.dev",
route => route.fulfill({ json: nodeInfo }),
);
await page.goto("/");
await expect(page.getByText("seed.radicle.garden")).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.radicle.garden" }),
).toBeVisible();
await page.getByPlaceholder("Navigate to seed URL").fill("seed.rhizoma.dev");
await page.getByPlaceholder("Navigate to seed URL").press("Enter");
await expect(page.getByText("seed.rhizoma.dev")).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.rhizoma.dev" }),
).toBeVisible();
// Test that removing the selected seed doesn't end in an undefined state.
await page
.getByRole("button", { name: "seed.rhizoma.dev" })
.getByRole("button")
.click();
await expect(page.getByText("seed.radicle.garden")).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.rhizoma.dev" }),
).toBeHidden();
});
test("stored custom preferred seeds in local storage", async ({ page }) => {
await page.addInitScript(() =>
localStorage.setItem(
"configuredPreferredSeeds",
'[{"hostname":"seed.radicle.xyz","port":443,"scheme":"https"},{"hostname":"seed.rhizoma.dev","port":443,"scheme":"https"}]',
),
);
await page.goto("/");
await expect(page.getByText("seed.radicle.xyz")).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.radicle.xyz" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "seed.rhizoma.dev" }),
).toBeVisible();
// Check that the fallback node hasn't been added on load.
await expect(
page.getByRole("button", { name: "seed.radicle.garden" }),
).toBeHidden();
});
const nodeInfo = {
id: "z6MkkGfMNQmjrp66Po2n4snzcSyTFRFw1m1fbYhCURxLxZpD",
agent: "/radicle:1.0.0-rc.11/",
config: {
alias: "rhizoma",
listen: [],
peers: {
type: "dynamic",
},
connect: [],
externalAddresses: ["seed.rhizoma.dev:8776"],
db: {
journalMode: "wal",
},
network: "main",
log: "INFO",
relay: "auto",
limits: {
routingMaxSize: 1000,
routingMaxAge: 604800,
gossipMaxAge: 1209600,
fetchConcurrency: 1,
maxOpenFiles: 4096,
rate: {
inbound: {
fillRate: 2,
capacity: 128,
},
outbound: {
fillRate: 5,
capacity: 256,
},
},
connection: {
inbound: 128,
outbound: 16,
},
},
workers: 32,
seedingPolicy: {
default: "block",
},
},
state: "running",
};
+72 -5
View File
@@ -21,11 +21,8 @@ test("node metadata", async ({ page, peerManager }) => {
await page.goto(peer.uiUrl());
await expect(page.getByRole("link", { name: "127.0.0.1" })).toBeVisible();
await expect(
page.getByText(`${shortNodeRemote}@seed.radicle.test:8123`),
).toBeVisible();
await expect(page.getByText("radicle:1.0.0-rc.11")).toBeVisible();
await expect(page.getByText(shortNodeRemote).first()).toBeVisible();
await expect(page.getByText("/radicle:1.0.0-rc.11/")).toBeVisible();
});
test("node projects", async ({ page }) => {
@@ -42,3 +39,73 @@ test("node projects", async ({ page }) => {
).toBeVisible();
}
});
test("show pinned repositories", async ({ page }) => {
await page.goto("/");
// Shows pinned project name.
await expect(page.getByText("source-browsing")).toBeVisible();
//
// Shows pinned project description.
await expect(
page.getByText("Git repository for source browsing tests"),
).toBeVisible();
});
test("no duplicate entry for preferred seeds", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("127.0.0.1").first()).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(page.getByRole("button", { name: "127.0.0.1" })).toBeVisible();
await page.getByPlaceholder("Navigate to seed").fill("127.0.0.1");
await page.getByPlaceholder("Navigate to seed").press("Enter");
await expect(page.getByText("Seed node already added.")).toBeVisible();
await page.getByPlaceholder("Navigate to seed").fill("");
await expect(page.getByText("Seed node already added.")).toBeHidden();
});
test("adding and removing a new preferred seed", async ({ page }) => {
// Proxy requests to seed.example.tld to the local test api.
await page.route(
url => url.hostname === "seed.example.tld",
route =>
route.fulfill({
status: 301,
headers: {
Location: route
.request()
.url()
.replace("seed.example.tld", "127.0.0.1"),
},
}),
);
await page.goto("/");
await expect(page.getByText("127.0.0.1").first()).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(page.getByRole("button", { name: "127.0.0.1" })).toBeVisible();
await page.getByPlaceholder("Navigate to seed").fill("seed.example.tld");
await page.getByPlaceholder("Navigate to seed").press("Enter");
await expect(page.getByText("seed.example.tld").first()).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.example.tld" }),
).toBeVisible();
// Test that removing the selected seed doesn't end in an undefined state.
await page
.getByRole("button", { name: "seed.example.tld" })
.getByRole("button")
.click();
await expect(page.getByText("127.0.0.1").first()).toBeVisible();
await page.getByTitle("Switch preferred seeds").getByRole("button").click();
await expect(
page.getByRole("button", { name: "seed.example.tld" }),
).toBeHidden();
});
-8
View File
@@ -13,9 +13,6 @@ describe("route invariant when parsed", () => {
scheme: "http",
};
test("home", () => {
return expectParsingInvariant({ resource: "home" });
});
test("nodes", () => {
expectParsingInvariant({
resource: "nodes",
@@ -213,11 +210,6 @@ describe("pathToRoute", () => {
expectPathToRoute("/foo/baz/bar", null);
});
test("home", () => {
expectPathToRoute("", { resource: "home" });
expectPathToRoute("/", { resource: "home" });
});
test("nodes", () => {
expectPathToRoute("/nodes/example.node.tld", {
resource: "nodes",
+1 -1
View File
@@ -17,7 +17,7 @@ test("node page", async ({ page }) => {
test("empty pinned projects", async ({ page }) => {
await page.route(
({ hostname, pathname }) =>
pathname === "/api/v1/projects" && hostname === "seed.radicle.garden",
pathname === "/api/v1/projects" && hostname === "127.0.0.1",
async route => {
await route.fulfill({
status: 200,