From 4ef0c88c2b49d9e02d7bc92a54b83642b0988b4e Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Thu, 5 Sep 2024 12:22:13 +0200 Subject: [PATCH] Add `list_repo` and `config` commands --- package-lock.json | 12 +--- package.json | 3 +- src-tauri/Cargo.toml | 2 +- src-tauri/bindings/Config.ts | 21 ++++++ src-tauri/bindings/RepoInfo.ts | 14 ++++ src-tauri/bindings/SupportedPayloads.ts | 25 +++++++ src-tauri/bindings/serde_json/JsonValue.ts | 7 ++ src-tauri/src/commands.rs | 3 + src-tauri/src/{ => commands}/auth.rs | 0 src-tauri/src/commands/profile.rs | 15 ++++ src-tauri/src/commands/repos.rs | 30 ++++++++ src-tauri/src/json.rs | 19 ++++++ src-tauri/src/lib.rs | 79 +++++++++++++++++++++- src-tauri/src/types/config.rs | 23 +++++++ src-tauri/src/types/mod.rs | 2 + src-tauri/src/types/repo.rs | 50 ++++++++++++++ src/App.svelte | 4 +- src/components/RepoCard.svelte | 28 +++++--- src/lib/api/author.ts | 8 --- src/lib/api/fixtures/heartwood-repo.json | 36 ---------- src/lib/api/repo.ts | 48 ------------- src/lib/router/definitions.ts | 17 ++++- src/views/Home.svelte | 30 ++------ 23 files changed, 330 insertions(+), 146 deletions(-) create mode 100644 src-tauri/bindings/Config.ts create mode 100644 src-tauri/bindings/RepoInfo.ts create mode 100644 src-tauri/bindings/SupportedPayloads.ts create mode 100644 src-tauri/bindings/serde_json/JsonValue.ts create mode 100644 src-tauri/src/commands.rs rename src-tauri/src/{ => commands}/auth.rs (100%) create mode 100644 src-tauri/src/commands/profile.rs create mode 100644 src-tauri/src/commands/repos.rs create mode 100644 src-tauri/src/json.rs create mode 100644 src-tauri/src/types/config.rs create mode 100644 src-tauri/src/types/mod.rs create mode 100644 src-tauri/src/types/repo.rs delete mode 100644 src/lib/api/author.ts delete mode 100644 src/lib/api/fixtures/heartwood-repo.json delete mode 100644 src/lib/api/repo.ts diff --git a/package-lock.json b/package-lock.json index 67286d5..0217f4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" - } } } } diff --git a/package.json b/package.json index 4ec291d..d81d13b 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9bd39d8..08b3246 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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 diff --git a/src-tauri/bindings/Config.ts b/src-tauri/bindings/Config.ts new file mode 100644 index 0000000..789e732 --- /dev/null +++ b/src-tauri/bindings/Config.ts @@ -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" }; +}; diff --git a/src-tauri/bindings/RepoInfo.ts b/src-tauri/bindings/RepoInfo.ts new file mode 100644 index 0000000..629f7e2 --- /dev/null +++ b/src-tauri/bindings/RepoInfo.ts @@ -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; +}; diff --git a/src-tauri/bindings/SupportedPayloads.ts b/src-tauri/bindings/SupportedPayloads.ts new file mode 100644 index 0000000..dad9ee1 --- /dev/null +++ b/src-tauri/bindings/SupportedPayloads.ts @@ -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; + }; + }; +}; diff --git a/src-tauri/bindings/serde_json/JsonValue.ts b/src-tauri/bindings/serde_json/JsonValue.ts new file mode 100644 index 0000000..1431f9a --- /dev/null +++ b/src-tauri/bindings/serde_json/JsonValue.ts @@ -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 + | { [key: string]: JsonValue }; diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..14f9d2f --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod profile; +pub mod repos; diff --git a/src-tauri/src/auth.rs b/src-tauri/src/commands/auth.rs similarity index 100% rename from src-tauri/src/auth.rs rename to src-tauri/src/commands/auth.rs diff --git a/src-tauri/src/commands/profile.rs b/src-tauri/src/commands/profile.rs new file mode 100644 index 0000000..5150c10 --- /dev/null +++ b/src-tauri/src/commands/profile.rs @@ -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) -> Result { + 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) +} diff --git a/src-tauri/src/commands/repos.rs b/src-tauri/src/commands/repos.rs new file mode 100644 index 0000000..62ddbe2 --- /dev/null +++ b/src-tauri/src/commands/repos.rs @@ -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) -> Result, Error> { + let storage = &ctx.profile.storage; + let policies = ctx.profile.policies()?; + + let mut repos = storage.repositories()?.into_iter().collect::>(); + 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::>(); + + Ok::<_, Error>(infos) +} diff --git a/src-tauri/src/json.rs b/src-tauri/src/json.rs new file mode 100644 index 0000000..dbca949 --- /dev/null +++ b/src-tauri/src/json.rs @@ -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, }), + ) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 23af978..0f47e14 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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( + &self, + repo: &R, + doc: DocAt, + ) -> Result { + 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::>(); + 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"); } diff --git a/src-tauri/src/types/config.rs b/src-tauri/src/types/config.rs new file mode 100644 index 0000000..5c22c3a --- /dev/null +++ b/src-tauri/src/types/config.rs @@ -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, +} diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs new file mode 100644 index 0000000..551ac1a --- /dev/null +++ b/src-tauri/src/types/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod repo; diff --git a/src-tauri/src/types/repo.rs b/src-tauri/src/types/repo.rs new file mode 100644 index 0000000..23e3506 --- /dev/null +++ b/src-tauri/src/types/repo.rs @@ -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, + 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, +} diff --git a/src/App.svelte b/src/App.svelte index 493a4d6..ec97540 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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"} {:else if $activeRouteStore.resource === "home"} - + {:else if $activeRouteStore.resource === "authenticationError"} {:else if $activeRouteStore.resource === "designSystem"} diff --git a/src/components/RepoCard.svelte b/src/components/RepoCard.svelte index 1b72e73..75d2b27 100644 --- a/src/components/RepoCard.svelte +++ b/src/components/RepoCard.svelte @@ -1,5 +1,5 @@