mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-04 03:00:32 +08:00
Redesign node view and use it as the landing-page
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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 |
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 />
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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" ||
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface HomeRoute {
|
||||
resource: "home";
|
||||
}
|
||||
|
||||
export interface HomeLoadedRoute extends HomeRoute {}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+31
-8
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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('')");
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user