Refactor seed view

Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Rūdolfs Ošiņš
2023-04-20 14:41:19 +02:00
parent 26419a74e2
commit 9704318248
8 changed files with 205 additions and 217 deletions
+6 -1
View File
@@ -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}
+32
View File
@@ -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>
-18
View File
@@ -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>
+3 -6
View File
@@ -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>
+9 -11
View File
@@ -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
View File
@@ -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>
-88
View File
@@ -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>
+3 -1
View File
@@ -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)}`),