mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
@@ -13,6 +13,7 @@
|
||||
|
||||
export let text: string;
|
||||
export let small = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -40,7 +41,11 @@
|
||||
</style>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span class="clipboard" class:small on:click|stopPropagation={copy}>
|
||||
<span
|
||||
title={tooltip}
|
||||
class="clipboard"
|
||||
class:small
|
||||
on:click|stopPropagation={copy}>
|
||||
{#if small}
|
||||
<Icon name="clipboard-small" />
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
|
||||
export let message: string;
|
||||
export let stackTrace: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
padding: 1rem;
|
||||
color: var(--color-negative);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-negative-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.stack-trace {
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="error">
|
||||
{message}
|
||||
{#if stackTrace}
|
||||
<div class="stack-trace">
|
||||
<Clipboard
|
||||
tooltip="Copy error to clipboard"
|
||||
text={JSON.stringify({ errorMessage: message, stackTrace }, null, 2)} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let error = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.message {
|
||||
padding: 1rem;
|
||||
}
|
||||
.message-error {
|
||||
color: var(--color-negative);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-negative-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="message" class:message-error={error}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -8,7 +8,7 @@
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import Message from "@app/components/Message.svelte";
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
import ProjectCard from "@app/components/ProjectCard.svelte";
|
||||
|
||||
function goToProject(project: Project, baseUrl: BaseUrl) {
|
||||
@@ -110,12 +110,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:catch}
|
||||
{:catch error}
|
||||
<div class="padding">
|
||||
<Message error>
|
||||
<span class="txt-bold">Error:</span>
|
||||
failed to load projects.
|
||||
</Message>
|
||||
<ErrorMessage message="Couldn't load projects." stackTrace={error} />
|
||||
</div>
|
||||
{/await}
|
||||
</main>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { sessionStore } from "@app/lib/session";
|
||||
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import Message from "@app/components/Message.svelte";
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
import NotFound from "@app/components/NotFound.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
history={history.commits.map(c => c.commit)} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
<ErrorMessage message="Couldn't load commits." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "commits"}
|
||||
@@ -217,7 +217,7 @@
|
||||
<Commit {commit} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
<ErrorMessage message="Couln't load commit." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "issues" && activeRoute.params.view.params?.view.resource === "new"}
|
||||
@@ -230,10 +230,8 @@
|
||||
{baseUrl} />
|
||||
{:else}
|
||||
<div class="message">
|
||||
<Message error>
|
||||
Could not access the issue creation. Make sure you're still logged
|
||||
in.
|
||||
</Message>
|
||||
<ErrorMessage
|
||||
message="Couldn't access issue creation. Make sure you're still logged in." />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if activeRoute.params.view.resource === "issues"}
|
||||
@@ -247,7 +245,7 @@
|
||||
{issues} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
<ErrorMessage message="Couldn't load issues." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "issue"}
|
||||
@@ -262,7 +260,7 @@
|
||||
{issue} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
<ErrorMessage message="Couldn't load issue." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "patches"}
|
||||
@@ -277,7 +275,7 @@
|
||||
projectPatches={project.patches} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
<ErrorMessage message="Couldn't load patches." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "patch"}
|
||||
@@ -293,7 +291,7 @@
|
||||
{patch} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
<ErrorMessage message="Couldn't load patch." stackTrace={e} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else}
|
||||
|
||||
+152
-92
@@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Project, NodeStats } from "@httpd-client";
|
||||
import type { WeeklyActivity } from "@app/lib/commit";
|
||||
|
||||
import { config } from "@app/lib/config";
|
||||
import * as router from "@app/lib/router";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { config } from "@app/lib/config";
|
||||
import { extractBaseUrl, isLocal, truncateId } from "@app/lib/utils";
|
||||
import { loadProjectActivity } from "@app/lib/commit";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import ErrorMessage from "@app/components/ErrorMessage.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import NotFound from "@app/components/NotFound.svelte";
|
||||
import Projects from "@app/views/seeds/View/Projects.svelte";
|
||||
import ProjectCard from "@app/components/ProjectCard.svelte";
|
||||
|
||||
export let hostnamePort: string;
|
||||
|
||||
@@ -18,22 +22,74 @@
|
||||
: baseUrl.hostname;
|
||||
const api = new HttpdClient(baseUrl);
|
||||
|
||||
const getProjectsAndStats = async (): Promise<{
|
||||
stats: NodeStats;
|
||||
projects: Project[];
|
||||
}> => {
|
||||
const stats = await api.getStats();
|
||||
const projects = await api.project.getAll({ page: 0, perPage: 10 });
|
||||
return { stats, projects };
|
||||
};
|
||||
const perPage = 10;
|
||||
let page = 0;
|
||||
let error: any;
|
||||
let loadingProjects = false;
|
||||
|
||||
let projects: Project[] = [];
|
||||
let nodeStats: NodeStats | undefined = undefined;
|
||||
let projectsWithActivity: { project: Project; activity: WeeklyActivity[] }[] =
|
||||
[];
|
||||
|
||||
async function loadProjects(): Promise<void> {
|
||||
loadingProjects = true;
|
||||
try {
|
||||
[nodeStats, projects] = await Promise.all([
|
||||
api.getStats(),
|
||||
api.project.getAll({ page, perPage }),
|
||||
]);
|
||||
|
||||
const results = await Promise.all(
|
||||
projects.map(async project => {
|
||||
const activity = await loadProjectActivity(project.id, baseUrl);
|
||||
return {
|
||||
project,
|
||||
activity,
|
||||
};
|
||||
}),
|
||||
);
|
||||
projectsWithActivity = [...projectsWithActivity, ...results];
|
||||
page += 1;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
loadingProjects = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goToProject(project: Project) {
|
||||
router.push({
|
||||
resource: "projects",
|
||||
params: {
|
||||
view: { resource: "tree" },
|
||||
id: project.id,
|
||||
hostnamePort:
|
||||
baseUrl.port === config.seeds.defaultHttpdPort
|
||||
? baseUrl.hostname
|
||||
: `${baseUrl.hostname}:${baseUrl.port}`,
|
||||
revision: undefined,
|
||||
hash: undefined,
|
||||
search: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$: showMoreButton =
|
||||
!loadingProjects &&
|
||||
!error &&
|
||||
nodeStats &&
|
||||
projectsWithActivity.length < nodeStats.projects.count;
|
||||
|
||||
loadProjects();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
padding: 5rem 0;
|
||||
.wrapper {
|
||||
width: 720px;
|
||||
margin: 5rem 0;
|
||||
}
|
||||
main > header {
|
||||
.header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
@@ -41,42 +97,28 @@
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 4fr 0fr;
|
||||
gap: 1rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.fields > div {
|
||||
place-self: center start;
|
||||
height: 2rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.seed-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
td {
|
||||
padding-bottom: 1.5rem;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
.seed-address {
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-regular);
|
||||
line-height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-foreground-6);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.more {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
main {
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.fields {
|
||||
grid-template-columns: 5rem auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,55 +126,73 @@
|
||||
<title>{hostName}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#await api.getRoot()}
|
||||
<main class="layout-centered">
|
||||
<Loading center />
|
||||
</main>
|
||||
{:then nodeInfo}
|
||||
<main>
|
||||
<header>
|
||||
<span class="title txt-title">
|
||||
<span class="txt-bold">
|
||||
{hostName}
|
||||
</span>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="fields">
|
||||
<!-- Seed Address -->
|
||||
<div class="txt-highlight">Address</div>
|
||||
<div class="seed-wrapper">
|
||||
<div class="seed-address">
|
||||
{truncateId(nodeInfo.node.id)}@{baseUrl.hostname}
|
||||
</div>
|
||||
<Clipboard
|
||||
small
|
||||
text={`${nodeInfo.node.id}@${baseUrl.hostname}:${config.seeds.defaultNodePort}`} />
|
||||
</div>
|
||||
<div class="layout-desktop" />
|
||||
<!-- API Version -->
|
||||
<div class="txt-highlight">Version</div>
|
||||
<div>{nodeInfo.version}</div>
|
||||
<div class="layout-desktop" />
|
||||
</div>
|
||||
<!-- Seed Projects -->
|
||||
{#await getProjectsAndStats()}
|
||||
<Loading center />
|
||||
{:then result}
|
||||
<Projects {baseUrl} projects={result.projects} stats={result.stats} />
|
||||
{:catch err}
|
||||
<div class="error txt-tiny">
|
||||
<div>
|
||||
API request to <span class="txt-monospace">{err.url}</span>
|
||||
failed.
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</main>
|
||||
{:catch}
|
||||
<div class="layout-centered">
|
||||
<NotFound
|
||||
title={baseUrl.hostname}
|
||||
subtitle="Not able to query information from this seed." />
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
<span class="txt-title txt-bold">
|
||||
{hostName}
|
||||
</span>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
{#await api.getRoot()}
|
||||
<Loading center />
|
||||
{:then nodeInfo}
|
||||
<table>
|
||||
<tr>
|
||||
<td class="txt-highlight">Address</td>
|
||||
<td>
|
||||
<div class="seed-address">
|
||||
{truncateId(nodeInfo.node.id)}@{baseUrl.hostname}
|
||||
<Clipboard
|
||||
small
|
||||
text={`${nodeInfo.node.id}@${baseUrl.hostname}:${config.seeds.defaultNodePort}`} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="txt-highlight">Version</td>
|
||||
<td>
|
||||
{nodeInfo.version}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{:catch error}
|
||||
<div style:margin-bottom="2rem">
|
||||
<ErrorMessage
|
||||
message="Not able to query information from this seed."
|
||||
stackTrace={error.stack} />
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<div style:margin-bottom="5rem">
|
||||
{#if projects}
|
||||
<div style:margin-top="1rem">
|
||||
{#each projectsWithActivity as { project, activity }}
|
||||
<div style:margin-bottom="0.5rem">
|
||||
<ProjectCard
|
||||
{activity}
|
||||
id={project.id}
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
head={project.head}
|
||||
on:click={() => goToProject(project)} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if loadingProjects}
|
||||
<div class="more">
|
||||
<Loading small />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showMoreButton}
|
||||
<div class="more">
|
||||
<Button variant="foreground" on:click={loadProjects}>More</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if error}
|
||||
<ErrorMessage
|
||||
message="Not able to load projects from this seed."
|
||||
stackTrace={error.stack} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl, Project, NodeStats } from "@httpd-client";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { config } from "@app/lib/config";
|
||||
import { loadProjectActivity } from "@app/lib/commit";
|
||||
|
||||
import List from "@app/components/List.svelte";
|
||||
import ProjectCard from "@app/components/ProjectCard.svelte";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let projects: Project[];
|
||||
export let stats: NodeStats;
|
||||
|
||||
const api = new HttpdClient(baseUrl);
|
||||
// A pointer to the current page of projects added to the listing
|
||||
let page = 0;
|
||||
|
||||
const fetchMoreProjects = async (): Promise<Project[]> => {
|
||||
try {
|
||||
stats = await api.getStats();
|
||||
const projects = await api.project.getAll({
|
||||
page: (page += 1),
|
||||
perPage: 10,
|
||||
});
|
||||
|
||||
if (projects.length > 0) {
|
||||
return projects;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// We return an empty array, for when no more projects are found, or an error is thrown
|
||||
// since List is looking for an iterable.
|
||||
return [];
|
||||
};
|
||||
|
||||
const onClick = (project: Project) => {
|
||||
router.push({
|
||||
resource: "projects",
|
||||
params: {
|
||||
view: { resource: "tree" },
|
||||
id: project.id,
|
||||
hostnamePort:
|
||||
baseUrl.port === config.seeds.defaultHttpdPort
|
||||
? baseUrl.hostname
|
||||
: `${baseUrl.hostname}:${baseUrl.port}`,
|
||||
revision: undefined,
|
||||
hash: undefined,
|
||||
search: undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.projects {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.projects .project {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="projects">
|
||||
<List
|
||||
bind:items={projects}
|
||||
complete={projects.length === stats.projects.count}
|
||||
query={fetchMoreProjects}>
|
||||
<svelte:fragment slot="list" let:items>
|
||||
{#each items as project}
|
||||
{#await loadProjectActivity(project.id, baseUrl) then activity}
|
||||
<div class="project">
|
||||
<ProjectCard
|
||||
{activity}
|
||||
id={project.id}
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
head={project.head}
|
||||
on:click={() => onClick(project)} />
|
||||
</div>
|
||||
{/await}
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</List>
|
||||
</div>
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
test("seed metadata", async ({ page }) => {
|
||||
await page.goto("/seeds/radicle.local");
|
||||
|
||||
await expect(page.locator("header").getByText("radicle.local")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".header").getByText("radicle.local"),
|
||||
).toBeVisible();
|
||||
await expect(page.locator("text=radicle.local")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(`text=${seedRemote.substring(0, 6)}…${seedRemote.slice(-6)}`),
|
||||
|
||||
Reference in New Issue
Block a user