feat: add dashboard

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-07 20:31:18 +01:00
parent b439762877
commit d46c365112
21 changed files with 2955 additions and 1367 deletions

View File

@@ -2,7 +2,8 @@ use forage_core::auth::{
AuthError, AuthTokens, CreatedToken, ForestAuth, PersonalAccessToken, User, UserEmail,
};
use forage_core::platform::{
Artifact, ArtifactContext, ForestPlatform, Organisation, PlatformError,
Artifact, ArtifactContext, ArtifactDestination, ArtifactSource, ForestPlatform, Organisation,
OrgMember, PlatformError,
};
use forage_grpc::organisation_service_client::OrganisationServiceClient;
use forage_grpc::release_service_client::ReleaseServiceClient;
@@ -292,6 +293,22 @@ fn convert_organisations(
fn convert_artifact(a: forage_grpc::Artifact) -> Artifact {
let ctx = a.context.unwrap_or_default();
let source = a.source.map(|s| ArtifactSource {
user: s.user.filter(|v| !v.is_empty()),
email: s.email.filter(|v| !v.is_empty()),
source_type: s.source_type.filter(|v| !v.is_empty()),
run_url: s.run_url.filter(|v| !v.is_empty()),
});
// Artifact proto does not carry git ref directly; git info comes from AnnotateRelease.
// We leave git_ref as None for now.
let destinations = a
.destinations
.into_iter()
.map(|d| ArtifactDestination {
name: d.name,
environment: d.environment,
})
.collect();
Artifact {
artifact_id: a.artifact_id,
slug: a.slug,
@@ -303,10 +320,22 @@ fn convert_artifact(a: forage_grpc::Artifact) -> Artifact {
ctx.description
},
},
source,
git_ref: None,
destinations,
created_at: a.created_at,
}
}
fn convert_member(m: forage_grpc::OrganisationMember) -> OrgMember {
OrgMember {
user_id: m.user_id,
username: m.username,
role: m.role,
joined_at: m.joined_at.map(|ts| ts.to_string()),
}
}
fn map_platform_status(status: tonic::Status) -> PlatformError {
match status.code() {
tonic::Code::Unauthenticated | tonic::Code::PermissionDenied => {
@@ -394,6 +423,131 @@ impl ForestPlatform for GrpcForestClient {
Ok(resp.artifact.into_iter().map(convert_artifact).collect())
}
async fn create_organisation(
&self,
access_token: &str,
name: &str,
) -> Result<String, PlatformError> {
let req = platform_authed_request(
access_token,
forage_grpc::CreateOrganisationRequest {
name: name.into(),
},
)?;
let resp = self
.org_client()
.create_organisation(req)
.await
.map_err(map_platform_status)?
.into_inner();
Ok(resp.organisation_id)
}
async fn list_members(
&self,
access_token: &str,
organisation_id: &str,
) -> Result<Vec<OrgMember>, PlatformError> {
let req = platform_authed_request(
access_token,
forage_grpc::ListMembersRequest {
organisation_id: organisation_id.into(),
page_size: 100,
page_token: String::new(),
},
)?;
let resp = self
.org_client()
.list_members(req)
.await
.map_err(map_platform_status)?
.into_inner();
Ok(resp.members.into_iter().map(convert_member).collect())
}
async fn add_member(
&self,
access_token: &str,
organisation_id: &str,
user_id: &str,
role: &str,
) -> Result<OrgMember, PlatformError> {
let req = platform_authed_request(
access_token,
forage_grpc::AddMemberRequest {
organisation_id: organisation_id.into(),
user_id: user_id.into(),
role: role.into(),
},
)?;
let resp = self
.org_client()
.add_member(req)
.await
.map_err(map_platform_status)?
.into_inner();
let member = resp
.member
.ok_or(PlatformError::Other("no member in response".into()))?;
Ok(convert_member(member))
}
async fn remove_member(
&self,
access_token: &str,
organisation_id: &str,
user_id: &str,
) -> Result<(), PlatformError> {
let req = platform_authed_request(
access_token,
forage_grpc::RemoveMemberRequest {
organisation_id: organisation_id.into(),
user_id: user_id.into(),
},
)?;
self.org_client()
.remove_member(req)
.await
.map_err(map_platform_status)?;
Ok(())
}
async fn update_member_role(
&self,
access_token: &str,
organisation_id: &str,
user_id: &str,
role: &str,
) -> Result<OrgMember, PlatformError> {
let req = platform_authed_request(
access_token,
forage_grpc::UpdateMemberRoleRequest {
organisation_id: organisation_id.into(),
user_id: user_id.into(),
role: role.into(),
},
)?;
let resp = self
.org_client()
.update_member_role(req)
.await
.map_err(map_platform_status)?
.into_inner();
let member = resp
.member
.ok_or(PlatformError::Other("no member in response".into()))?;
Ok(convert_member(member))
}
}
#[cfg(test)]