Add list_repo and config commands

This commit is contained in:
Sebastian Martinez
2024-09-05 12:37:00 +02:00
parent 3edfcf9192
commit b6711a5698
23 changed files with 332 additions and 146 deletions
+1 -11
View File
@@ -32,8 +32,7 @@
"tslib": "^2.7.0",
"typescript": "^5.2.2",
"typescript-eslint": "^8.4.0",
"vite": "^5.4.2",
"zod": "^3.23.8"
"vite": "^5.4.2"
},
"engines": {
"node": "20.9.0"
@@ -3220,15 +3219,6 @@
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"dev": true
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+1 -2
View File
@@ -40,7 +40,6 @@
"tslib": "^2.7.0",
"typescript": "^5.2.2",
"typescript-eslint": "^8.4.0",
"vite": "^5.4.2",
"zod": "^3.23.8"
"vite": "^5.4.2"
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ tauri = { version = "2.0.0-rc.0", features = ["isolation"] }
tauri-plugin-shell = { version = "2.0.0-rc.0" }
tauri-plugin-window-state = "2.0.0-rc.1"
thiserror = { version = "1.0.63" }
ts-rs = { version = "9.0.1", features = ["serde-json-impl"] }
ts-rs = { version = "9.0.1", features = ["serde-json-impl", "no-serde-warnings"] }
[features]
# by default Tauri runs in production mode
+21
View File
@@ -0,0 +1,21 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Service configuration.
*/
export type Config = {
/**
* Node alias.
*/
publicKey: string;
/**
* Node alias.
*/
alias: string;
/**
* Default seeding policy.
*/
seedingPolicy:
| { default: "allow"; scope: "followed" | "all" }
| { default: "block" };
};
+14
View File
@@ -0,0 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SupportedPayloads } from "./SupportedPayloads";
/**
* Repos info.
*/
export type RepoInfo = {
payloads: SupportedPayloads;
delegates: ({ id: string } | { id: string; alias?: string })[];
threshold: number;
visibility: { type: "public" } | { type: "private"; allow?: string[] };
rid: string;
seeding: number;
};
+25
View File
@@ -0,0 +1,25 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SupportedPayloads = {
"xyz.radicle.project"?: {
data: {
defaultBranch: string;
description: string;
name: string;
};
meta: {
head: string;
issues: {
open: number;
closed: number;
};
patches: {
open: number;
draft: number;
archived: number;
merged: number;
};
lastCommit: number;
};
};
};
@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type JsonValue =
| number
| string
| Array<JsonValue>
| { [key: string]: JsonValue };
+3
View File
@@ -0,0 +1,3 @@
pub mod auth;
pub mod profile;
pub mod repos;
+15
View File
@@ -0,0 +1,15 @@
use crate::error::Error;
use crate::types::config::Config;
use crate::AppState;
/// Get active config.
#[tauri::command]
pub fn config(ctx: tauri::State<AppState>) -> Result<Config, Error> {
let config = Config {
public_key: ctx.profile.public_key,
alias: ctx.profile.config.node.alias.clone(),
seeding_policy: ctx.profile.config.node.seeding_policy,
};
Ok::<_, Error>(config)
}
+30
View File
@@ -0,0 +1,30 @@
use radicle::storage::ReadStorage;
use crate::error::Error;
use crate::types;
use crate::AppState;
/// List all repos.
#[tauri::command]
pub fn list_repos(ctx: tauri::State<AppState>) -> Result<Vec<types::repo::RepoInfo>, Error> {
let storage = &ctx.profile.storage;
let policies = ctx.profile.policies()?;
let mut repos = storage.repositories()?.into_iter().collect::<Vec<_>>();
repos.sort_by_key(|p| p.rid);
let infos = repos
.into_iter()
.filter_map(|info| {
if !policies.is_seeding(&info.rid).unwrap_or_default() {
return None;
}
let (repo, doc) = ctx.repo(info.rid).ok()?;
let repo_info = ctx.repo_info(&repo, doc).ok()?;
Some(repo_info)
})
.collect::<Vec<_>>();
Ok::<_, Error>(infos)
}
+19
View File
@@ -0,0 +1,19 @@
use serde_json::{json, Value};
use radicle::identity;
use radicle::node::AliasStore;
pub(crate) struct Author<'a>(&'a identity::Did);
impl<'a> Author<'a> {
pub fn new(did: &'a identity::Did) -> Self {
Self(did)
}
pub fn as_json(&self, aliases: &impl AliasStore) -> Value {
aliases.alias(self.0).map_or(
json!({ "id": self.0 }),
|alias| json!({ "id": self.0, "alias": alias, }),
)
}
}
+76 -3
View File
@@ -1,13 +1,82 @@
mod auth;
mod commands;
mod error;
mod json;
mod types;
use auth::authenticate;
use serde_json::json;
use tauri::Manager;
use radicle::identity::doc::PayloadId;
use radicle::identity::DocAt;
use radicle::identity::RepoId;
use radicle::issue::cache::Issues;
use radicle::node::routing::Store;
use radicle::patch::cache::Patches;
use radicle::storage::git::Repository;
use radicle::storage::{ReadRepository, ReadStorage};
use commands::{auth, profile, repos};
use types::repo::SupportedPayloads;
struct AppState {
profile: radicle::Profile,
}
impl AppState {
pub fn repo_info<R: ReadRepository + radicle::cob::Store>(
&self,
repo: &R,
doc: DocAt,
) -> Result<types::repo::RepoInfo, error::Error> {
let DocAt { doc, .. } = doc;
let rid = repo.id();
let aliases = self.profile.aliases();
let delegates = doc
.delegates
.into_iter()
.map(|did| json::Author::new(&did).as_json(&aliases))
.collect::<Vec<_>>();
let db = &self.profile.database()?;
let seeding = db.count(&rid).unwrap_or_default();
let project = doc.payload.get(&PayloadId::project()).and_then(|payload| {
let (_, head) = repo.head().ok()?;
let commit = repo.commit(head).ok()?;
let patches = self.profile.patches(repo).ok()?;
let patches = patches.counts().ok()?;
let issues = self.profile.issues(repo).ok()?;
let issues = issues.counts().ok()?;
Some(json!({
"data": payload,
"meta": {
"issues": issues,
"patches": patches,
"head": head,
"lastCommit": commit.time().seconds(),
},
}))
});
Ok(types::repo::RepoInfo {
payloads: SupportedPayloads { project },
delegates,
threshold: doc.threshold,
visibility: doc.visibility,
rid,
seeding,
})
}
/// Get a repository by RID, checking to make sure we're allowed to view it.
pub fn repo(&self, rid: RepoId) -> Result<(Repository, DocAt), error::Error> {
let repo = self.profile.storage.repository(rid)?;
let doc = repo.identity_doc()?;
Ok((repo, doc))
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -30,7 +99,11 @@ pub fn run() {
})
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.invoke_handler(tauri::generate_handler![authenticate])
.invoke_handler(tauri::generate_handler![
auth::authenticate,
repos::list_repos,
profile::config,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+23
View File
@@ -0,0 +1,23 @@
use radicle::crypto::PublicKey;
use serde::Serialize;
use ts_rs::TS;
use radicle::node::config::DefaultSeedingPolicy;
use radicle::node::Alias;
/// Service configuration.
#[derive(Debug, Clone, TS, Serialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Config {
/// Node Public Key in NID format.
#[ts(as = "String")]
pub public_key: PublicKey,
/// Node alias.
#[ts(as = "String")]
pub alias: Alias,
/// Default seeding policy.
#[serde(default)]
#[ts(type = "{ default: 'allow', scope: 'followed' | 'all' } | { default: 'block' }")]
pub seeding_policy: DefaultSeedingPolicy,
}
+2
View File
@@ -0,0 +1,2 @@
pub mod config;
pub mod repo;
+50
View File
@@ -0,0 +1,50 @@
use serde::Serialize;
use serde_json::Value;
use ts_rs::TS;
use radicle::identity::RepoId;
/// Repos info.
#[derive(Serialize, TS)]
#[ts(export)]
pub struct RepoInfo {
pub payloads: SupportedPayloads,
#[ts(type = "({ id: string } | { id: string, alias?: string })[]")]
pub delegates: Vec<Value>,
pub threshold: usize,
#[ts(type = "{ type: 'public' } | { type: 'private', allow?: string[] }")]
pub visibility: radicle::identity::Visibility,
#[ts(as = "String")]
pub rid: RepoId,
pub seeding: usize,
}
#[derive(Serialize, TS)]
#[ts(export)]
pub struct SupportedPayloads {
#[serde(rename = "xyz.radicle.project")]
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(type = r#"{
data: {
defaultBranch: string,
description: string,
name: string,
},
meta: {
head: string,
issues: {
open: number,
closed: number,
},
patches: {
open: number,
draft: number,
archived: number,
merged: number,
}
lastCommit: number,
}
}"#)]
pub project: Option<Value>,
}
+2 -2
View File
@@ -7,9 +7,9 @@
import { theme } from "@app/components/ThemeSwitch.svelte";
import { unreachable } from "@app/lib/utils";
import AuthenticationError from "@app/views/AuthenticationError.svelte";
import DesignSystem from "@app/views/DesignSystem.svelte";
import Home from "@app/views/Home.svelte";
import AuthenticationError from "@app/views/AuthenticationError.svelte";
const activeRouteStore = router.activeRouteStore;
void router.loadFromLocation();
@@ -36,7 +36,7 @@
{#if $activeRouteStore.resource === "booting"}
<!-- Don't show anything -->
{:else if $activeRouteStore.resource === "home"}
<Home />
<Home {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "authenticationError"}
<AuthenticationError {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "designSystem"}
+17 -11
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Repo } from "@app/lib/api/repo";
import type { RepoInfo } from "@bindings/RepoInfo";
import { formatRepositoryId, formatTimestamp } from "@app/lib/utils";
@@ -7,13 +7,10 @@
import Fill from "./Fill.svelte";
import Icon from "./Icon.svelte";
// TODO: Pass this via repo.
export let updatedAt: number = 1725360130;
export let repo: Repo;
export let repo: RepoInfo;
export let selfDid: string;
$: project = repo.payloads["xyz.radicle.project"];
$: project = repo.payloads["xyz.radicle.project"]!;
</script>
<style>
@@ -66,17 +63,26 @@
</div>
</div>
<div class="title">Radicle Heartwood Protocol & Stack</div>
<div class="title">
{#if project.data.description}
{project.data.description}
{:else}
No description
{/if}
</div>
<div class="global-oid">{formatRepositoryId(repo.rid)}</div>
<div class="global-flex footer">
<div class="global-flex">
<div class="global-flex" style:gap="4px"><Icon name="issue" /> 4</div>
<div class="global-flex" style:gap="4px"><Icon name="patch" /> 6</div>
<div class="global-flex" style:gap="4px">
<Icon name="issue" />{project.meta.issues.open}
</div>
<div class="global-flex" style:gap="4px">
<Icon name="patch" />{project.meta.patches.open}
</div>
</div>
<span style:color="var(--color-fill-gray)">
Updated {formatTimestamp(updatedAt)}
Updated {formatTimestamp(project.meta.lastCommit)}
</span>
</div>
</div>
-8
View File
@@ -1,8 +0,0 @@
import { object, string, z } from "zod";
export type Author = z.infer<typeof authorSchema>;
export const authorSchema = object({
id: string(),
alias: string().optional(),
});
-36
View File
@@ -1,36 +0,0 @@
{
"rid": "rad:z3trNYnLWS11cJWC6BbxDs5niGo82",
"payloads": {
"xyz.radicle.project": {
"data": {
"name": "rips",
"description": "Radicle Improvement Proposals (RIPs)",
"defaultBranch": "master"
},
"meta": {
"head": "329dee9a4b65169ea3889a7da239892b705d0d68",
"patches": {
"open": 0,
"draft": 2,
"archived": 0,
"merged": 0
},
"issues": {
"open": 1,
"closed": 0
}
}
}
},
"delegates": [
{
"id": "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"alias": "cloudhead"
}
],
"threshold": 1,
"visibility": {
"type": "private"
},
"seeding": 29
}
-48
View File
@@ -1,48 +0,0 @@
import {
array,
literal,
number,
object,
optional,
string,
union,
z,
} from "zod";
import { authorSchema } from "./author";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const repoSchema = object({
rid: string(),
payloads: object({
"xyz.radicle.project": object({
data: object({
name: string(),
description: string(),
defaultBranch: string(),
}),
meta: object({
head: string(),
patches: object({
open: number(),
draft: number(),
archived: number(),
merged: number(),
}),
issues: object({
open: number(),
closed: number(),
}),
}),
}),
}),
delegates: array(authorSchema),
threshold: number(),
visibility: union([
object({ type: literal("public") }),
object({ type: literal("private"), allow: optional(array(string())) }),
]),
seeding: number(),
});
export type Repo = z.infer<typeof repoSchema>;
+16 -1
View File
@@ -1,3 +1,8 @@
import type { RepoInfo } from "@bindings/RepoInfo";
import type { Config } from "@bindings/Config";
import { invoke } from "@tauri-apps/api/core";
interface BootingRoute {
resource: "booting";
}
@@ -14,6 +19,11 @@ interface HomeRoute {
resource: "home";
}
interface LoadedHomeRoute {
resource: "home";
params: { repos: RepoInfo[]; config: Config };
}
interface DesignSystemRoute {
resource: "designSystem";
}
@@ -27,12 +37,17 @@ export type Route =
export type LoadedRoute =
| BootingRoute
| DesignSystemRoute
| HomeRoute
| LoadedHomeRoute
| AuthenticationErrorRoute;
export async function loadRoute(
route: Route,
_previousLoaded: LoadedRoute,
): Promise<LoadedRoute> {
if (route.resource === "home") {
const repos: RepoInfo[] = await invoke("list_repos");
const config: Config = await invoke("config");
return { resource: "home", params: { repos, config } };
}
return route;
}
+9 -23
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import type { Repo } from "@app/lib/api/repo";
import repoFixture from "@app/lib/api/fixtures/heartwood-repo.json";
import type { Config } from "@bindings/Config";
import type { RepoInfo } from "@bindings/RepoInfo";
import Header from "@app/components/Header.svelte";
import Link from "@app/components/Link.svelte";
import RepoCard from "@app/components/RepoCard.svelte";
const selfDid = "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT";
export let repos: RepoInfo[];
export let config: Config;
</script>
<style>
@@ -24,25 +24,11 @@
<Header currentPage="Repositories" />
<div class="layout">
<div class="repo-grid">
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
<RepoCard repo={repoFixture as Repo} {selfDid} />
{#each repos as repo}
{#if repo.payloads["xyz.radicle.project"]}
<RepoCard {repo} selfDid={`did:key:${config.publicKey}`} />
{/if}
{/each}
</div>
<div style:margin-top="1rem">