~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
300 lines · 9.3 KB Raw History
<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>