Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/web/src/components/ActorDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// DrFed: A web-based platform for developing and debugging ActivityPub apps
// Copyright (C) 2026 DrFed team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import { graphql } from "relay-runtime";
import { Show } from "solid-js";
import { createFragment } from "solid-relay";

import type { ActorDetail_actor$key } from "./__generated__/ActorDetail_actor.graphql.ts";

import styles from "~/styles/instance.module.css";

export const ActorDetail = (props: { $actor: ActorDetail_actor$key }) => {
const actorData = createFragment(
graphql`
fragment ActorDetail_actor on Actor {
handle
}
`,
() => props.$actor,
);

return (
<Show when={actorData()}>
{(actor) => (
<article class={styles.actorCard}>
<span class={styles.actorMarker} aria-hidden="true" />
<p>{actor().handle}</p>
</article>
)}
</Show>
);
};
21 changes: 14 additions & 7 deletions packages/web/src/components/InstanceSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export default function InstanceSummary(props: {
fragment InstanceSummary_instance on Instance {
id
host
localInstance {
slug
}
}
`,
() => props.$instance,
Expand All @@ -45,13 +48,17 @@ export default function InstanceSummary(props: {
<p class={styles.cardLabel}>Instance host</p>
<h2>{instance().host}</h2>
</div>
<Link
class={styles.cardAction}
href={`/workspace/create/${instance().id}/actors`}
>
Create actors
<span aria-hidden="true">→</span>
</Link>
<Show when={instance().localInstance}>
{(localInstance) => (
<Link
class={styles.cardAction}
href={`/instance/${localInstance().slug}`}
>
View Detail
<span aria-hidden="true">→</span>
</Link>
)}
</Show>
</article>
)}
</Show>
Expand Down
223 changes: 147 additions & 76 deletions packages/web/src/routes/instance/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,87 +15,158 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import { Title } from "@solidjs/meta";
import { A, useParams } from "@solidjs/router";
import { For } from "solid-js";
import {
A,
type RouteDefinition,
type RouteSectionProps,
query,
} from "@solidjs/router";
import { graphql } from "relay-runtime";
import { For, Show } from "solid-js";
import {
createPreloadedQuery,
loadQuery,
useRelayEnvironment,
} from "solid-relay";

import { ActorDetail } from "~/components/ActorDetail.tsx";

import type { InstanceDetailQuery } from "./__generated__/InstanceDetailQuery.graphql.ts";

import styles from "~/styles/instance.module.css";

const actors = ["@sherry", "@newsroom", "@garden"] as const;
// Temp Query to show first 100.
const instanceDetailQuery = graphql`
query InstanceDetailQuery($slug: String!) {
localInstanceBySlug(slug: $slug) {
instance {
id
host
actors(first: 100) {
totalCount
edges {
node {
...ActorDetail_actor
}
}
}
}
}
}
`;

const loadInstanceDetailQuery = query(
(slug: string) =>
loadQuery<InstanceDetailQuery>(
useRelayEnvironment()(),
instanceDetailQuery,
{ slug },
),
"InstanceDetailQuery",
);

export const route = {
preload({ params }) {
if (params.slug === undefined) {
throw new Error("Missing slug route parameter.");
}

return loadInstanceDetailQuery(params.slug);
},
} satisfies RouteDefinition;

type RouteData = ReturnType<typeof loadInstanceDetailQuery>;

export default function InstanceDetailPage() {
const params = useParams();
const host = () => params.slug ?? "social.example";
export default function InstanceDetailPage(
props: RouteSectionProps<RouteData>,
) {
const data = createPreloadedQuery<InstanceDetailQuery>(
instanceDetailQuery,
() => props.data,
);

const instanceData = () => data()?.localInstanceBySlug?.instance;

return (
<main class={styles.page}>
<Title>{host()} — DrFed</Title>

<A class={styles.backLink} href="/workspace">
<span aria-hidden="true">←</span> All instances
</A>

<header class={styles.header}>
<h1>{host()}</h1>
<A
class={styles.createButton}
href="/workspace/create/instance-demo/actors"
>
<span aria-hidden="true">+</span>
Create actor
</A>
</header>

<section class={styles.details} aria-labelledby="connection-title">
<div>
<p class={styles.sectionLabel}>Connection record</p>
<h2 id="connection-title">Federation endpoints</h2>
</div>
<dl class={styles.endpointList}>
<div>
<dt>NodeInfo</dt>
<dd>
<a href={`https://${host()}/nodeinfo/2.1`}>/nodeinfo/2.1</a>
</dd>
</div>
<div>
<dt>WebFinger</dt>
<dd>
<a href={`https://${host()}/.well-known/webfinger`}>
/.well-known/webfinger
</a>
</dd>
</div>
<div>
<dt>Shared inbox</dt>
<dd>
<a href={`https://${host()}/inbox`}>/inbox</a>
</dd>
</div>
</dl>
</section>

<section class={styles.actors} aria-labelledby="actors-title">
<header class={styles.sectionHeader}>
<div>
<p class={styles.sectionLabel}>Local identities</p>
<h2 id="actors-title">Actors</h2>
</div>
<p>{actors.length} registered</p>
</header>

<div class={styles.actorList}>
<For each={actors}>
{(handle) => (
<article class={styles.actorCard}>
<span class={styles.actorMarker} aria-hidden="true" />
<p>
{handle}@{host()}
</p>
</article>
)}
</For>
</div>
</section>
</main>
<Show
when={instanceData()}
fallback={
<main class={styles.page}>
<A class={styles.backLink} href="/workspace">
<span aria-hidden="true">←</span> All instances
</A>
<p>Instance not found.</p>
</main>
}
>
{(instance) => (
<main class={styles.page}>
<Title>{instance().host} — DrFed</Title>

<A class={styles.backLink} href="/workspace">
<span aria-hidden="true">←</span> All instances
</A>

<header class={styles.header}>
<h1>{instance().host}</h1>
<A
class={styles.createButton}
href={`/workspace/create/${instance().id}/actors`}
>
<span aria-hidden="true">+</span>
Create actor
</A>
</header>

<section class={styles.details} aria-labelledby="connection-title">
<div>
<p class={styles.sectionLabel}>Connection record</p>
<h2 id="connection-title">Federation endpoints</h2>
</div>
<dl class={styles.endpointList}>
<div>
<dt>NodeInfo</dt>
<dd>
<a href={`https://${instance().host}/nodeinfo/2.1`}>
{`https://${instance().host}/nodeinfo/2.1`}
</a>
</dd>
</div>
<div>
<dt>WebFinger</dt>
<dd>
<a href={`https://${instance().host}/.well-known/webfinger`}>
{`https://${instance().host}/.well-known/webfinger`}
</a>
</dd>
</div>
<div>
<dt>Shared inbox</dt>
<dd>
<a
href={`https://${instance().host}/inbox`}
>{`https://${instance().host}/inbox`}</a>
</dd>
</div>
</dl>
</section>

<section class={styles.actors} aria-labelledby="actors-title">
<header class={styles.sectionHeader}>
<div>
<p class={styles.sectionLabel}>Local identities</p>
<h2 id="actors-title">Actors</h2>
</div>
<p>{instance().actors.totalCount} registered</p>
</header>
<div class={styles.actorList}>
<For each={instance().actors.edges}>
{(edge) => <ActorDetail $actor={edge.node} />}
</For>
</div>
</section>
</main>
)}
</Show>
);
}