johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<script lang="ts">
import type { ActorView } from '#lib/lexicons/dev/knotless/defs.ts';
import type { TriagerView } from '#lib/lexicons/dev/knotless/getRepoSettings.ts';
import type * as removeTriager from '#lib/lexicons/dev/knotless/removeTriager.ts';
import type * as setTriager from '#lib/lexicons/dev/knotless/setTriager.ts';
import type * as shareRepo from '#lib/lexicons/dev/knotless/shareRepo.ts';
import type * as unshareRepo from '#lib/lexicons/dev/knotless/unshareRepo.ts';
import type { AtIdentifierString, AtUriString } from '@atproto/lex';
import { invalidateAll } from '$app/navigation';
import { call } from '#lib/data.ts';
import Avatar from '#lib/atoms/Avatar.svelte';
import Delegate from '#lib/molecules/Delegate.svelte';
import Person from '#lib/molecules/Person.svelte';
import Empty from '#lib/molecules/Empty.svelte';
import HandleInput from '#lib/molecules/HandleInput.svelte';
/**
* Who may read the repository `uri` names, if it is `private`: its
* `readers`, as `knotless repo share` and `unshare` change them; who may
* push to it, its `collaborators`, as `knotless repo share --write` changes
* them, through the `gateway` its owner delegated to, from the pages
* (`delegated`) as `knotless account delegate` does; and whose labels count,
* its `triagers`, as `knotless triager list` names them.
*/
let {
uri,
collaborators,
triagers,
gateway,
delegated,
private: hidden = false,
readers = []
}: {
uri: AtUriString;
collaborators: ActorView[];
triagers: TriagerView[];
gateway?: string;
delegated?: string;
private?: boolean;
readers?: ActorView[];
} = $props();
let who = $state('');
let reader = $state('');
let readProblem = $state('');
let gotDelegated = $state(false);
const through = $derived(gateway || delegated || gotDelegated);
let triager = $state('');
let scopes = $state('');
let problem = $state('');
// Why a private repository's new triager was not told of it, if it was not.
let triageWarning = $state('');
let triageProblem = $state('');
// What is under way: which form is adding, or whose removal.
let busy = $state('');
/** Does `act`, as `action`, then reads the settings again; or says, with `said`, why it could not. */
async function doing(action: string, act: () => Promise<unknown>, said: (why: string) => void) {
busy = action;
said('');
try {
await act();
await invalidateAll();
return true;
} catch (err) {
said((err as Error).message);
return false;
} finally {
busy = '';
}
}
const sayShare = (why: string) => (problem = why);
const sayTriage = (why: string) => (triageProblem = why);
const sayRead = (why: string) => (readProblem = why);
async function letRead(e: SubmitEvent) {
e.preventDefault();
const shared = () =>
call<typeof shareRepo.main>('dev.knotless.shareRepo', {
repo: uri,
with: reader.trim() as AtIdentifierString,
write: false
});
if (await doing('read', shared, sayRead)) reader = '';
}
function unread(a: ActorView) {
doing(
`unread ${a.did}`,
() => call<typeof unshareRepo.main>('dev.knotless.unshareRepo', { repo: uri, with: a.did }),
sayRead
);
}
async function share(e: SubmitEvent) {
e.preventDefault();
const shared = () =>
call<typeof shareRepo.main>('dev.knotless.shareRepo', {
repo: uri,
with: who.trim() as AtIdentifierString
});
if (await doing('share', shared, sayShare)) who = '';
}
function unshare(a: ActorView) {
doing(
`unshare ${a.did}`,
() => call<typeof unshareRepo.main>('dev.knotless.unshareRepo', { repo: uri, with: a.did }),
sayShare
);
}
async function name(e: SubmitEvent) {
e.preventDefault();
const named = scopes
.split(/[\s,]+/)
.filter(Boolean)
.map((s) => ({ name: s }));
triageWarning = '';
const set = async () => {
const after = await call<typeof setTriager.main>('dev.knotless.setTriager', {
repo: uri,
who: triager.trim() as AtIdentifierString,
scopes: named
});
triageWarning = after.warning ?? '';
};
if (await doing('triage', set, sayTriage)) triager = scopes = '';
}
function unname(t: TriagerView) {
doing(
`untriage ${t.actor.did}`,
() =>
call<typeof removeTriager.main>('dev.knotless.removeTriager', {
repo: uri,
who: t.actor.did
}),
sayTriage
);
}
</script>
{#if hidden}
<section class="setting">
<h2>Readers</h2>
<p class="hint">
This repository is private: only its owner, its collaborators and the accounts named here can
see it, and open pulls and issues on it. Its runners read it too, to run its checks.
</p>
{#if readers.length}
<ul class="box list rows">
{#each readers as a (a.did)}
<li>
<span class="row-main"
><Avatar name={a.handle} did={a.did} src={a.avatar} /><Person
handle={a.handle}
/></span
><span class="row-end"
><button
class="btn danger"
type="button"
disabled={!!busy}
aria-busy={busy === `unread ${a.did}`}
onclick={() => unread(a)}
>{busy === `unread ${a.did}` ? 'Removing…' : 'Remove'}</button
></span
>
</li>
{/each}
</ul>
{:else}
<div class="box">
<Empty title="No readers yet">Only the owner and collaborators can see it.</Empty>
</div>
{/if}
<form class="form" onsubmit={letRead}>
<label for="share-read">Add a reader</label>
<div class="owned">
<HandleInput id="share-read" name="with" autocomplete="off" bind:value={reader} /><button
class="btn"
type="submit"
disabled={!!busy}
aria-busy={busy === 'read'}>{busy === 'read' ? 'Adding…' : 'Add'}</button
>
</div>
{#if readProblem}<p class="problem" role="alert">{readProblem}</p>{/if}
</form>
</section>
{/if}
<section class="setting">
<h2>Collaborators</h2>
<p class="hint">
Collaborators push, commit and merge, through the gateway the owner delegated writes to, which
holds them to the protected branches.
</p>
{#if collaborators.length}
<ul class="box list rows">
{#each collaborators as a (a.did)}
<li>
<span class="row-main"
><Avatar name={a.handle} did={a.did} src={a.avatar} /><Person handle={a.handle} /></span
><span class="row-end"
><button
class="btn danger"
type="button"
disabled={!!busy}
aria-busy={busy === `unshare ${a.did}`}
onclick={() => unshare(a)}
>{busy === `unshare ${a.did}` ? 'Removing…' : 'Remove'}</button
></span
>
</li>
{/each}
</ul>
{:else}
<div class="box"><Empty title="No collaborators yet">Only the owner pushes.</Empty></div>
{/if}
<form class="form" onsubmit={share}>
<label for="share-with">Add a collaborator</label>
<div class="owned">
<HandleInput id="share-with" name="with" autocomplete="off" bind:value={who} /><button
class="btn"
type="submit"
disabled={!!busy}
aria-busy={busy === 'share'}>{busy === 'share' ? 'Adding…' : 'Add'}</button
>
</div>
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
</form>
{#if through}
<p class="hint">
{gateway
? `Collaborators push through ${gateway}.`
: 'Your writes are delegated to this server: collaborators you add push through it.'}
</p>
{:else}
<Delegate done={() => (gotDelegated = true)} />
<p class="hint">
Before adding a collaborator, let this server write for you, for all your repositories: you
approve it at your PDS, for knotless's records alone, and collaborators push through it, held
to your protected branches. You can also do this in <a href="/settings/writes"
>your settings</a
>.
</p>
{/if}
</section>
<section class="setting">
<h2>Triagers</h2>
<p class="hint">
A triager, such as a bot, labels pulls and issues within the scopes you name. When the owner or
a collaborator labels too, theirs wins.
</p>
{#if triagers.length}
<ul class="box list rows">
{#each triagers as t (t.actor.did)}
<li>
<span class="row-main"
><Avatar name={t.actor.handle} did={t.actor.did} src={t.actor.avatar} /><Person
handle={t.actor.handle}
/><span class="mute">{t.scopes.map((s) => s.name).join(', ')}</span></span
><span class="row-end"
><button
class="btn danger"
type="button"
disabled={!!busy}
aria-busy={busy === `untriage ${t.actor.did}`}
onclick={() => unname(t)}
>{busy === `untriage ${t.actor.did}` ? 'Removing…' : 'Remove'}</button
></span
>
</li>
{/each}
</ul>
{:else}
<div class="box"><Empty title="No triagers yet" /></div>
{/if}
<form class="form" onsubmit={name}>
<label for="triager-who">Add a triager</label>
<input
id="triager-who"
name="who"
placeholder="triage-bot.example.com"
autocapitalize="none"
spellcheck="false"
required
bind:value={triager}
/>
<label for="triager-scopes">Scopes</label>
<div class="owned">
<input
id="triager-scopes"
name="scopes"
placeholder="kind, priority"
autocapitalize="none"
spellcheck="false"
required
bind:value={scopes}
/><button class="btn" type="submit" disabled={!!busy} aria-busy={busy === 'triage'}
>{busy === 'triage' ? 'Adding…' : 'Add'}</button
>
</div>
<p class="hint">A scope is a label's name up to its last <code>/</code>: kind for kind/bug.</p>
{#if triageProblem}<p class="problem" role="alert">{triageProblem}</p>{/if}
{#if triageWarning}<p class="warning" role="status">{triageWarning}</p>{/if}
</form>
</section>