Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,998 @@
|
||||
use std::{borrow::Cow, fmt, str::FromStr};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use derivative::Derivative;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use galaxy_core::{
|
||||
features::FeatureFlag,
|
||||
ui::{Icon, appearance::Appearance, theme::Fill},
|
||||
};
|
||||
use galaxy_graphql::{object_permissions::AccessLevel, scalars::time::ServerTimestamp};
|
||||
use galaxyui_core::{
|
||||
Element,
|
||||
elements::{
|
||||
Align, ChildAnchor, ConstrainedBox, Hoverable, MouseStateHandle, OffsetPositioning,
|
||||
ParentAnchor, ParentElement, ParentOffsetBounds, Stack,
|
||||
},
|
||||
ui_components::components::UiComponent,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::UserUid,
|
||||
drive::sharing::{SharingAccessLevel, Subject, TeamKind, UserKind},
|
||||
ids::{FolderId, ServerId, SyncId},
|
||||
};
|
||||
|
||||
/// The type of object id each ObjectType corresponds to.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ObjectIdType {
|
||||
Notebook,
|
||||
Workflow,
|
||||
Folder,
|
||||
GenericStringObject,
|
||||
}
|
||||
|
||||
impl ObjectIdType {
|
||||
/// Returns the prefix for server IDs as we store them in sqlite. The prefix for these
|
||||
/// objects is in title case unlike how we store the object types, which is why two different
|
||||
/// APIs are needed.
|
||||
pub fn sqlite_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
ObjectIdType::Notebook => "Notebook",
|
||||
ObjectIdType::Workflow => "Workflow",
|
||||
ObjectIdType::Folder => "Folder",
|
||||
ObjectIdType::GenericStringObject => "GenericStringObject",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for communicating the type of cloud object to/from the server, absent of the object itself.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub enum ObjectType {
|
||||
Notebook,
|
||||
Workflow,
|
||||
Folder,
|
||||
GenericStringObject(GenericStringObjectFormat),
|
||||
}
|
||||
|
||||
impl ObjectType {
|
||||
/// Returns the serialized string for the object type, to be used for storing object_type in sqlite.
|
||||
pub fn sqlite_object_type_as_str(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
ObjectType::Notebook => "NOTEBOOK".into(),
|
||||
ObjectType::Workflow => "WORKFLOW".into(),
|
||||
ObjectType::Folder => "FOLDER".into(),
|
||||
ObjectType::GenericStringObject(format) => format.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NOTEBOOK_OBJECT_STRING: &str = "notebook";
|
||||
const WORKFLOW_OBJECT_STRING: &str = "workflow";
|
||||
const PROMPT_OBJECT_STRING: &str = "prompt";
|
||||
const FOLDER_OBJECT_STRING: &str = "folder";
|
||||
const ENV_VAR_COLLECTION_STRING: &str = "env-vars";
|
||||
|
||||
impl FromStr for ObjectType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
NOTEBOOK_OBJECT_STRING => Ok(Self::Notebook),
|
||||
WORKFLOW_OBJECT_STRING => Ok(Self::Workflow),
|
||||
PROMPT_OBJECT_STRING => Ok(Self::Workflow),
|
||||
FOLDER_OBJECT_STRING => Ok(Self::Folder),
|
||||
ENV_VAR_COLLECTION_STRING => Ok(Self::GenericStringObject(
|
||||
GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection),
|
||||
)),
|
||||
_ => Err(anyhow!("Unexpected object type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ObjectType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ObjectType::Notebook => write!(f, "{NOTEBOOK_OBJECT_STRING}"),
|
||||
ObjectType::Workflow => write!(f, "{WORKFLOW_OBJECT_STRING}"),
|
||||
ObjectType::Folder => write!(f, "{FOLDER_OBJECT_STRING}"),
|
||||
ObjectType::GenericStringObject(GenericStringObjectFormat::Json(
|
||||
JsonObjectType::EnvVarCollection,
|
||||
)) => write!(f, "{ENV_VAR_COLLECTION_STRING}"),
|
||||
ObjectType::GenericStringObject(GenericStringObjectFormat::Json(
|
||||
JsonObjectType::AIFact,
|
||||
)) => write!(f, "rule"),
|
||||
ObjectType::GenericStringObject(_) => write!(f, "string_object_placeholder"), // placeholder value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectType> for ObjectIdType {
|
||||
fn from(value: ObjectType) -> Self {
|
||||
match value {
|
||||
ObjectType::Notebook => ObjectIdType::Notebook,
|
||||
ObjectType::Workflow => ObjectIdType::Workflow,
|
||||
ObjectType::Folder => ObjectIdType::Folder,
|
||||
ObjectType::GenericStringObject(_) => ObjectIdType::GenericStringObject,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The object type prefix for generic string objects.
|
||||
pub const GENERIC_STRING_OBJECT_PREFIX: &str = "GENERIC_STRING_";
|
||||
|
||||
/// The object type prefix for json objects.
|
||||
pub const JSON_OBJECT_PREFIX: &str = "JSON_";
|
||||
|
||||
/// The data format for the generic string object type.
|
||||
/// Right now we only support json, but this is left
|
||||
/// open to support markdown, yaml and other text based types.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub enum GenericStringObjectFormat {
|
||||
Json(JsonObjectType),
|
||||
}
|
||||
|
||||
// Temporarily suppress clippy warnings about the `ToString` impl until we
|
||||
// move `ObjectType` away from using `std::fmt::Display` for serialization.
|
||||
#[allow(clippy::to_string_trait_impl)]
|
||||
impl ToString for GenericStringObjectFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
GenericStringObjectFormat::Json(json_object_type) => format!(
|
||||
"{}{}{}",
|
||||
GENERIC_STRING_OBJECT_PREFIX,
|
||||
JSON_OBJECT_PREFIX,
|
||||
json_object_type.as_str()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object sub-type for objects that implement the JsonModel trait.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub enum JsonObjectType {
|
||||
Preference,
|
||||
EnvVarCollection,
|
||||
WorkflowEnum,
|
||||
AIFact,
|
||||
MCPServer,
|
||||
AIExecutionProfile,
|
||||
TemplatableMCPServer,
|
||||
CloudEnvironment,
|
||||
ScheduledAmbientAgent,
|
||||
CloudAgentConfig,
|
||||
}
|
||||
|
||||
impl JsonObjectType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
JsonObjectType::Preference => "PREFERENCE",
|
||||
JsonObjectType::EnvVarCollection => "ENVVARCOLLECTION",
|
||||
JsonObjectType::WorkflowEnum => "WORKFLOWENUM",
|
||||
JsonObjectType::AIFact => "AIFACT",
|
||||
JsonObjectType::MCPServer => "MCPSERVER",
|
||||
JsonObjectType::AIExecutionProfile => "AIEXECUTIONPROFILE",
|
||||
JsonObjectType::TemplatableMCPServer => "TEMPLATABLEMCPSERVER",
|
||||
JsonObjectType::CloudEnvironment => "CLOUDENVIRONMENT",
|
||||
JsonObjectType::ScheduledAmbientAgent => "SCHEDULEDAMBIENTAGENT",
|
||||
JsonObjectType::CloudAgentConfig => "CLOUDAGENTCONFIG",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for JsonObjectType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
"PREFERENCE" => Ok(JsonObjectType::Preference),
|
||||
"ENVVARCOLLECTION" => Ok(JsonObjectType::EnvVarCollection),
|
||||
"WORKFLOWENUM" => Ok(JsonObjectType::WorkflowEnum),
|
||||
"AIFACT" => Ok(JsonObjectType::AIFact),
|
||||
"MCPSERVER" => Ok(JsonObjectType::MCPServer),
|
||||
"AIEXECUTIONPROFILE" => Ok(JsonObjectType::AIExecutionProfile),
|
||||
"TEMPLATABLEMCPSERVER" => Ok(JsonObjectType::TemplatableMCPServer),
|
||||
"CLOUDENVIRONMENT" => Ok(JsonObjectType::CloudEnvironment),
|
||||
"SCHEDULEDAMBIENTAGENT" => Ok(JsonObjectType::ScheduledAmbientAgent),
|
||||
"CLOUDAGENTCONFIG" => Ok(JsonObjectType::CloudAgentConfig),
|
||||
_ => Err(anyhow!("could not convert unknown json object type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object::ObjectType> for ObjectIdType {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(object_type: galaxy_graphql::object::ObjectType) -> Result<Self, Self::Error> {
|
||||
match object_type {
|
||||
galaxy_graphql::object::ObjectType::AIConversation => Err(anyhow!(
|
||||
"AIConversation is not a supported object type for this operation"
|
||||
)),
|
||||
galaxy_graphql::object::ObjectType::Notebook => Ok(ObjectIdType::Notebook),
|
||||
galaxy_graphql::object::ObjectType::Workflow => Ok(ObjectIdType::Workflow),
|
||||
galaxy_graphql::object::ObjectType::Folder => Ok(ObjectIdType::Folder),
|
||||
galaxy_graphql::object::ObjectType::GenericStringObject => {
|
||||
Ok(ObjectIdType::GenericStringObject)
|
||||
}
|
||||
galaxy_graphql::object::ObjectType::Unknown => {
|
||||
Err(anyhow!("could not convert unknown cloud object type"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectType> for galaxy_graphql::object::ObjectType {
|
||||
fn from(value: ObjectType) -> Self {
|
||||
match value {
|
||||
ObjectType::Notebook => galaxy_graphql::object::ObjectType::Notebook,
|
||||
ObjectType::Workflow => galaxy_graphql::object::ObjectType::Workflow,
|
||||
ObjectType::Folder => galaxy_graphql::object::ObjectType::Folder,
|
||||
ObjectType::GenericStringObject(GenericStringObjectFormat::Json(
|
||||
JsonObjectType::EnvVarCollection,
|
||||
)) => galaxy_graphql::object::ObjectType::GenericStringObject,
|
||||
ObjectType::GenericStringObject(gso) => {
|
||||
todo!("Moving is not implemented for {:?}", gso);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The revision timestamp at which an object was edited. This is used by the server
|
||||
/// to determine if an edit to an object was at the latest revision. Edits at older
|
||||
/// revisions are rejected by the server.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub struct Revision(ServerTimestamp);
|
||||
|
||||
impl Revision {
|
||||
pub fn from_unix_timestamp_micros(ms_since_epoch: i64) -> Result<Self> {
|
||||
let ts = ServerTimestamp::from_unix_timestamp_micros(ms_since_epoch)?;
|
||||
Ok(Self(ts))
|
||||
}
|
||||
|
||||
pub fn timestamp_micros(&self) -> i64 {
|
||||
self.0.timestamp_micros()
|
||||
}
|
||||
|
||||
pub fn utc(&self) -> DateTime<Utc> {
|
||||
self.0.utc()
|
||||
}
|
||||
|
||||
/// Returns the inner `ServerTimestamp`.
|
||||
pub fn timestamp(&self) -> ServerTimestamp {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub fn now() -> Self {
|
||||
Self(ServerTimestamp::new(Utc::now()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Revision> for ServerTimestamp {
|
||||
fn from(revision: Revision) -> Self {
|
||||
revision.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerTimestamp> for Revision {
|
||||
fn from(time: ServerTimestamp) -> Self {
|
||||
Revision(time)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
impl From<DateTime<Utc>> for Revision {
|
||||
fn from(time: DateTime<Utc>) -> Self {
|
||||
Self(ServerTimestamp::new(time))
|
||||
}
|
||||
}
|
||||
|
||||
/// The owner for a given object.
|
||||
#[derive(Copy, Clone, Debug, Eq, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(PartialEq)]
|
||||
pub enum Owner {
|
||||
/// The owner of the object is a user (the object is in their personal drive).
|
||||
User { user_uid: UserUid },
|
||||
/// The owner of the object is a team (the object is in a team drive).
|
||||
Team { team_uid: ServerId },
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
/// A mock [`Owner`] ID for testing.
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub fn mock_current_user() -> Owner {
|
||||
use crate::auth::TEST_USER_UID;
|
||||
|
||||
Owner::User {
|
||||
user_uid: UserUid::new(TEST_USER_UID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Owner> for Option<ServerId> {
|
||||
fn from(owner: Owner) -> Option<ServerId> {
|
||||
match owner {
|
||||
Owner::User { .. } => None,
|
||||
Owner::Team { team_uid, .. } => Some(team_uid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Server representation of an object's container. This corresponds to the `Container` GraphQL
|
||||
/// type.
|
||||
///
|
||||
/// Containers are similar to, but not quite the same as, the [`CloudObjectLocation`] type.
|
||||
/// Locations depend on object and user state - an object might currently be in the trash, or
|
||||
/// it could be in one user's [shared space](Space::Shared) but another's
|
||||
/// [team space](Space::Team). Containers, on the other hand, represent an object's canonical
|
||||
/// parent - its one parent folder or drive that permissions are inherited from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServerObjectContainer {
|
||||
Folder { folder_uid: ServerId },
|
||||
Drive { owner: Owner },
|
||||
}
|
||||
|
||||
/// Server representation of a user object guest, as part of [`ServerObjectGuest`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ServerGuestSubject {
|
||||
User { firebase_uid: String },
|
||||
PendingUser { email: Option<String> },
|
||||
Team { team_uid: ServerId },
|
||||
}
|
||||
|
||||
/// Server representation of a link-sharing setting.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ServerLinkSharing {
|
||||
pub access_level: AccessLevel,
|
||||
pub source: Option<ServerObjectContainer>,
|
||||
}
|
||||
|
||||
/// Server representation of an object guest. This corresponds to the `ObjectGuest` GraphQL type.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ServerObjectGuest {
|
||||
pub subject: ServerGuestSubject,
|
||||
pub access_level: AccessLevel,
|
||||
/// If this guest is inherited, this is the ancestor that it's inherited from.
|
||||
pub source: Option<ServerObjectContainer>,
|
||||
}
|
||||
|
||||
/// Metadata for a cloud object that was fetched from the server.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerMetadata {
|
||||
pub uid: ServerId,
|
||||
pub revision: Revision,
|
||||
pub metadata_last_updated_ts: ServerTimestamp,
|
||||
pub trashed_ts: Option<ServerTimestamp>,
|
||||
pub folder_id: Option<FolderId>,
|
||||
pub is_welcome_object: bool,
|
||||
pub creator_uid: Option<String>,
|
||||
pub last_editor_uid: Option<String>,
|
||||
pub current_editor_uid: Option<String>,
|
||||
}
|
||||
|
||||
/// Permissions for a cloud object that was fetched from the server.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ServerPermissions {
|
||||
/// The GraphQL definition of a `Space` is closer to the client's definition of an `Owner` (due
|
||||
/// to sharing). This is also going to migrate back to [ServerMetadata] as part of the
|
||||
/// `Container` migration.
|
||||
pub space: Owner,
|
||||
pub guests: Vec<ServerObjectGuest>,
|
||||
pub anyone_link_sharing: Option<ServerLinkSharing>,
|
||||
pub permissions_last_updated_ts: ServerTimestamp,
|
||||
}
|
||||
|
||||
impl ServerPermissions {
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub fn mock_personal() -> Self {
|
||||
Self {
|
||||
space: Owner::mock_current_user(),
|
||||
guests: Vec::new(),
|
||||
anyone_link_sharing: None,
|
||||
permissions_last_updated_ts: DateTime::<Utc>::default().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NumInFlightRequests(pub usize);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// An enum representing what state a local cloud object's content changes can be in,
|
||||
/// in relation to the server.
|
||||
pub enum CloudObjectSyncStatus {
|
||||
/// The object's content hasn't changed from what we believe the server's representation
|
||||
/// to be.
|
||||
NoLocalChanges,
|
||||
/// The object's content has been modified locally, and is currently in the sync queue
|
||||
/// attempting to sync up with the server.
|
||||
InFlight(NumInFlightRequests),
|
||||
/// The object's content has been modified locally but has unresolved conflict with the server
|
||||
/// revision.
|
||||
InConflict,
|
||||
/// The object's content has been modified locally, but persisting the change on the server
|
||||
/// could not complete for some reason.
|
||||
Errored,
|
||||
}
|
||||
|
||||
const SYNC_ICON_DIMENSIONS: f32 = 16.;
|
||||
|
||||
const SYNC_STATUS_TOOLTIP_LOCAL_ONLY: &str = "Saved locally";
|
||||
const SYNC_STATUS_TOOLTIP_INFLIGHT: &str = "Saving";
|
||||
const SYNC_STATUS_TOOLTIP_ERROR: &str = "Failed to save";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CloudObjectPermissions {
|
||||
pub owner: Owner,
|
||||
pub permissions_last_updated_ts: Option<ServerTimestamp>,
|
||||
pub anyone_with_link: Option<CloudLinkSharing>,
|
||||
pub guests: Vec<CloudObjectGuest>,
|
||||
}
|
||||
|
||||
impl CloudObjectPermissions {
|
||||
pub fn new_from_server(server_permissions: ServerPermissions) -> Self {
|
||||
let guests = if FeatureFlag::SharedWithMe.is_enabled() {
|
||||
server_permissions
|
||||
.guests
|
||||
.into_iter()
|
||||
.map(CloudObjectGuest::from_server)
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let anyone_with_link = if FeatureFlag::SharedWithMe.is_enabled() {
|
||||
server_permissions
|
||||
.anyone_link_sharing
|
||||
.map(CloudLinkSharing::from_server)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
owner: server_permissions.space,
|
||||
permissions_last_updated_ts: Some(server_permissions.permissions_last_updated_ts),
|
||||
guests,
|
||||
anyone_with_link,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock permissions for a personal object.
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub fn mock_personal() -> Self {
|
||||
Self {
|
||||
owner: Owner::mock_current_user(),
|
||||
permissions_last_updated_ts: Some(Utc::now().into()),
|
||||
guests: Vec::new(),
|
||||
anyone_with_link: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given user has direct personal access to this object —
|
||||
/// either via an explicit user guest ACL entry or via link sharing.
|
||||
/// Returns `false` if the only access is through a team guest ACL.
|
||||
pub fn has_direct_user_access(&self, user_uid: UserUid) -> bool {
|
||||
self.anyone_with_link.is_some() || self.guests.iter().any(|g| g.subject.is_user(user_uid))
|
||||
}
|
||||
|
||||
/// Updates self from new permissions information received from the server
|
||||
pub fn update_from_new_permissions_ts(&mut self, server_permissions: ServerPermissions) {
|
||||
self.owner = server_permissions.space;
|
||||
self.permissions_last_updated_ts = Some(server_permissions.permissions_last_updated_ts);
|
||||
if FeatureFlag::SharedWithMe.is_enabled() {
|
||||
self.guests = server_permissions
|
||||
.guests
|
||||
.into_iter()
|
||||
.map(CloudObjectGuest::from_server)
|
||||
.collect();
|
||||
self.anyone_with_link = server_permissions
|
||||
.anyone_link_sharing
|
||||
.map(CloudLinkSharing::from_server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CloudLinkSharing {
|
||||
pub access_level: SharingAccessLevel,
|
||||
// If this sharing setting was inherited, the `source` identifies the container it's inherited
|
||||
// from.
|
||||
pub source: Option<ServerObjectContainer>,
|
||||
}
|
||||
|
||||
impl CloudLinkSharing {
|
||||
pub fn from_server(server_link_sharing: ServerLinkSharing) -> Self {
|
||||
Self {
|
||||
access_level: server_link_sharing.access_level.into(),
|
||||
source: server_link_sharing.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CloudObjectGuest {
|
||||
pub subject: Subject,
|
||||
pub access_level: SharingAccessLevel,
|
||||
/// If this guest was added to a container object, the `source` identifies that object.
|
||||
pub source: Option<ServerObjectContainer>,
|
||||
}
|
||||
|
||||
impl CloudObjectGuest {
|
||||
pub fn from_server(server_guest: ServerObjectGuest) -> Self {
|
||||
let subject = match server_guest.subject {
|
||||
ServerGuestSubject::User { firebase_uid } => {
|
||||
Subject::User(UserKind::Account(UserUid::new(&firebase_uid)))
|
||||
}
|
||||
ServerGuestSubject::PendingUser { email } => Subject::PendingUser { email },
|
||||
ServerGuestSubject::Team { team_uid } => Subject::Team(TeamKind::Team { team_uid }),
|
||||
};
|
||||
|
||||
Self {
|
||||
subject,
|
||||
access_level: server_guest.access_level.into(),
|
||||
source: server_guest.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CloudObjectMetadata {
|
||||
pub revision: Option<Revision>,
|
||||
pub metadata_last_updated_ts: Option<ServerTimestamp>,
|
||||
pub current_editor_uid: Option<String>,
|
||||
pub pending_changes_statuses: CloudObjectStatuses,
|
||||
pub trashed_ts: Option<ServerTimestamp>,
|
||||
pub folder_id: Option<SyncId>,
|
||||
/// Welcome objects are created on the server when a user first recieves
|
||||
/// access to Warp Drive as part of onboarding.
|
||||
pub is_welcome_object: bool,
|
||||
pub last_editor_uid: Option<String>,
|
||||
pub creator_uid: Option<String>,
|
||||
/// The "last used" timestamp for this environment.
|
||||
///
|
||||
/// This is populated via `GetCloudEnvironments` from
|
||||
/// `CloudEnvironment.lastTaskCreated.createdAt`.
|
||||
/// Only applicable for CloudEnvironment objects.
|
||||
pub last_task_run_ts: Option<ServerTimestamp>,
|
||||
}
|
||||
|
||||
impl CloudObjectMetadata {
|
||||
pub fn new_from_server(server_metadata: ServerMetadata) -> Self {
|
||||
Self {
|
||||
revision: Some(server_metadata.revision),
|
||||
current_editor_uid: server_metadata.current_editor_uid,
|
||||
metadata_last_updated_ts: Some(server_metadata.metadata_last_updated_ts),
|
||||
pending_changes_statuses: CloudObjectStatuses {
|
||||
content_sync_status: CloudObjectSyncStatus::NoLocalChanges,
|
||||
has_pending_metadata_change: false,
|
||||
has_pending_permissions_change: false,
|
||||
pending_untrash: false,
|
||||
pending_delete: false,
|
||||
},
|
||||
trashed_ts: server_metadata.trashed_ts,
|
||||
folder_id: server_metadata.folder_id.map(|id| id.into()),
|
||||
is_welcome_object: server_metadata.is_welcome_object,
|
||||
creator_uid: server_metadata.creator_uid,
|
||||
last_editor_uid: server_metadata.last_editor_uid,
|
||||
// last_task_run_ts is populated separately via GetCloudEnvironments query
|
||||
last_task_run_ts: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new set of metadata with reasonable defaults for a test:
|
||||
/// * Content and metadata timestamps set to now
|
||||
/// * No editor information
|
||||
/// * No parent folder
|
||||
/// * Not trashed
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub fn mock() -> Self {
|
||||
Self {
|
||||
revision: Some(Revision::now()),
|
||||
current_editor_uid: None,
|
||||
metadata_last_updated_ts: Some(Utc::now().into()),
|
||||
pending_changes_statuses: CloudObjectStatuses::mock(),
|
||||
trashed_ts: None,
|
||||
folder_id: None,
|
||||
is_welcome_object: false,
|
||||
last_editor_uid: None,
|
||||
creator_uid: None,
|
||||
last_task_run_ts: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_content_changes(&self) -> bool {
|
||||
!matches!(
|
||||
self.pending_changes_statuses.content_sync_status,
|
||||
CloudObjectSyncStatus::NoLocalChanges | CloudObjectSyncStatus::InConflict
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_errored(&self) -> bool {
|
||||
matches!(
|
||||
self.pending_changes_statuses.content_sync_status,
|
||||
CloudObjectSyncStatus::Errored
|
||||
)
|
||||
}
|
||||
|
||||
/// True iff there are unsynced online-only changes for the object.
|
||||
pub fn has_pending_online_only_change(&self) -> bool {
|
||||
self.pending_changes_statuses.has_pending_permissions_change
|
||||
|| self.pending_changes_statuses.has_pending_metadata_change
|
||||
|| self.pending_changes_statuses.pending_untrash
|
||||
|| self.pending_changes_statuses.pending_delete
|
||||
}
|
||||
|
||||
pub fn set_current_editor(&mut self, editor_uid: Option<String>) {
|
||||
self.current_editor_uid = editor_uid;
|
||||
}
|
||||
|
||||
/// Updates revision and last_editor_uid from server metadata.
|
||||
///
|
||||
/// This unconditionally updates the revision and last_editor_uid, even if
|
||||
/// there are conflicts, so callers should check for conflicts before calling
|
||||
/// this.
|
||||
pub fn update_revision_from_server(&mut self, server_metadata: &ServerMetadata) {
|
||||
self.revision = Some(server_metadata.revision.clone());
|
||||
self.last_editor_uid = server_metadata.last_editor_uid.clone();
|
||||
}
|
||||
|
||||
/// Updates self from a new metadata received from the server
|
||||
pub fn update_from_new_metadata_ts(&mut self, server_metadata: ServerMetadata) {
|
||||
// Overwriting the metadata from an MetadataUpdated RTC message shouldn't overwrite
|
||||
// the versioning of the object's data: the revision timestamp, has_pending_changes, conflict_status
|
||||
// (if the object data is not being updated, the data versioning should stay the same.
|
||||
self.current_editor_uid = server_metadata.current_editor_uid;
|
||||
self.trashed_ts = server_metadata.trashed_ts;
|
||||
self.folder_id = server_metadata.folder_id.map(|folder_id| folder_id.into());
|
||||
self.creator_uid = server_metadata.creator_uid;
|
||||
self.metadata_last_updated_ts = Some(server_metadata.metadata_last_updated_ts);
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct holding the different statuses of pending changes that a cloud object might have.
|
||||
/// Note that content is handled differently than permissions/metadata:
|
||||
/// * Content changes go through the sync queue, and thus can exist in more states
|
||||
/// * Metadata/permissions changes are synchronous operations, and thus are only either
|
||||
/// in flight or synced
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CloudObjectStatuses {
|
||||
pub content_sync_status: CloudObjectSyncStatus,
|
||||
/// True iff there are unsynced permission changes for the object.
|
||||
/// We intentionally don't persist this value in sqlite. And if true,
|
||||
/// we don't upsert any in-memory permission changes to sqlite.
|
||||
pub has_pending_permissions_change: bool,
|
||||
/// True iff there are unsynced metadata changes for the object.
|
||||
/// We intentionally don't persist this value in sqlite. And if true,
|
||||
/// we don't upsert trashed and folder changes to sqlite.
|
||||
pub has_pending_metadata_change: bool,
|
||||
|
||||
/// True iff there is an unsynced untrash operation on the object.
|
||||
pub pending_untrash: bool,
|
||||
|
||||
/// True iff there is an unsynced delete operation on the object.
|
||||
pub pending_delete: bool,
|
||||
}
|
||||
|
||||
impl CloudObjectStatuses {
|
||||
/// Empty statuses with no in-flight changes, for use in tests.
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub fn mock() -> Self {
|
||||
Self {
|
||||
content_sync_status: CloudObjectSyncStatus::NoLocalChanges,
|
||||
has_pending_permissions_change: false,
|
||||
has_pending_metadata_change: false,
|
||||
pending_untrash: false,
|
||||
pending_delete: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_icon(
|
||||
&self,
|
||||
sync_queue_is_dequeueing: bool,
|
||||
hover_state: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let theme = appearance.theme();
|
||||
let has_in_flight_requests = match &self.content_sync_status {
|
||||
CloudObjectSyncStatus::InFlight(reqs) => reqs.0 > 0,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let should_show_local_only_indicator = has_in_flight_requests && !sync_queue_is_dequeueing;
|
||||
let should_show_syncing_indicator = has_in_flight_requests
|
||||
|| self.has_pending_metadata_change
|
||||
|| self.has_pending_permissions_change
|
||||
|| self.pending_untrash;
|
||||
let should_show_error_indicator = matches!(
|
||||
self.content_sync_status,
|
||||
CloudObjectSyncStatus::Errored | CloudObjectSyncStatus::InConflict
|
||||
);
|
||||
|
||||
let icon_and_tooltip_text = if should_show_local_only_indicator {
|
||||
Some((
|
||||
Icon::Laptop.to_galaxyui_icon(theme.main_text_color(theme.surface_1())),
|
||||
SYNC_STATUS_TOOLTIP_LOCAL_ONLY,
|
||||
))
|
||||
} else if should_show_syncing_indicator {
|
||||
Some((
|
||||
Icon::Refresh.to_galaxyui_icon(theme.sub_text_color(theme.surface_2())),
|
||||
SYNC_STATUS_TOOLTIP_INFLIGHT,
|
||||
))
|
||||
} else if should_show_error_indicator {
|
||||
Some((
|
||||
Icon::AlertTriangle.to_galaxyui_icon(Fill::Solid(theme.ui_error_color())),
|
||||
SYNC_STATUS_TOOLTIP_ERROR,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some((icon, tooltip_text)) = icon_and_tooltip_text {
|
||||
return Some(
|
||||
Align::new(
|
||||
Hoverable::new(hover_state, move |hover_state| {
|
||||
let mut stack = Stack::new().with_child(
|
||||
ConstrainedBox::new(icon.finish())
|
||||
.with_height(SYNC_ICON_DIMENSIONS)
|
||||
.with_width(SYNC_ICON_DIMENSIONS)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if hover_state.is_hovered() {
|
||||
let tooltip = appearance
|
||||
.ui_builder()
|
||||
.tool_tip(tooltip_text.to_string())
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
stack.add_positioned_overlay_child(
|
||||
tooltip,
|
||||
OffsetPositioning::offset_from_parent(
|
||||
vec2f(0., -24.),
|
||||
ParentOffsetBounds::Unbounded,
|
||||
ParentAnchor::Center,
|
||||
ChildAnchor::Center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
stack.finish()
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Used for event tracking purposes, matches
|
||||
// up with GraphQL enum of the same name.
|
||||
#[derive(Copy, Default, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum CloudObjectEventEntrypoint {
|
||||
TeamSettings,
|
||||
ResourceCenter,
|
||||
UniversalSearch,
|
||||
ManagementUI,
|
||||
Blocklist,
|
||||
ImportModal,
|
||||
Onboarding,
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
// GraphQL conversion impls.
|
||||
|
||||
impl From<GenericStringObjectFormat>
|
||||
for galaxy_graphql::generic_string_object::GenericStringObjectFormat
|
||||
{
|
||||
fn from(format: GenericStringObjectFormat) -> Self {
|
||||
use galaxy_graphql::generic_string_object::GenericStringObjectFormat as GraphQLFormat;
|
||||
match format {
|
||||
GenericStringObjectFormat::Json(JsonObjectType::Preference) => {
|
||||
GraphQLFormat::JsonPreference
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::EnvVarCollection) => {
|
||||
GraphQLFormat::JsonEnvVarCollection
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::WorkflowEnum) => {
|
||||
GraphQLFormat::JsonWorkflowEnum
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::AIFact) => GraphQLFormat::JsonAIFact,
|
||||
GenericStringObjectFormat::Json(JsonObjectType::MCPServer) => {
|
||||
GraphQLFormat::JsonMCPServer
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::AIExecutionProfile) => {
|
||||
GraphQLFormat::JsonAIExecutionProfile
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::TemplatableMCPServer) => {
|
||||
GraphQLFormat::JsonTemplatableMCPServer
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::CloudEnvironment) => {
|
||||
GraphQLFormat::JsonCloudEnvironment
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::ScheduledAmbientAgent) => {
|
||||
GraphQLFormat::JsonScheduledAmbientAgent
|
||||
}
|
||||
GenericStringObjectFormat::Json(JsonObjectType::CloudAgentConfig) => {
|
||||
unreachable!("JsonCloudAgentConfig is no longer present in GraphQL schema")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CloudObjectEventEntrypoint> for galaxy_graphql::object::CloudObjectEventEntrypoint {
|
||||
fn from(entrypoint: CloudObjectEventEntrypoint) -> Self {
|
||||
use galaxy_graphql::object::CloudObjectEventEntrypoint as GraphQLEntrypoint;
|
||||
match entrypoint {
|
||||
CloudObjectEventEntrypoint::TeamSettings => GraphQLEntrypoint::TeamSettings,
|
||||
CloudObjectEventEntrypoint::ResourceCenter => GraphQLEntrypoint::ResourceCenter,
|
||||
CloudObjectEventEntrypoint::UniversalSearch => GraphQLEntrypoint::UniversalSearch,
|
||||
CloudObjectEventEntrypoint::ManagementUI => GraphQLEntrypoint::DriveIndex,
|
||||
CloudObjectEventEntrypoint::Blocklist => GraphQLEntrypoint::Blocklist,
|
||||
CloudObjectEventEntrypoint::ImportModal => GraphQLEntrypoint::ImportModal,
|
||||
CloudObjectEventEntrypoint::Onboarding => GraphQLEntrypoint::Onboarding,
|
||||
CloudObjectEventEntrypoint::Unknown => GraphQLEntrypoint::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object::ObjectMetadata> for ServerMetadata {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: galaxy_graphql::object::ObjectMetadata) -> Result<Self, Self::Error> {
|
||||
let folder_id: Option<FolderId> = match value.parent {
|
||||
galaxy_graphql::object::Container::FolderContainer(folder_container) => {
|
||||
Some(folder_container.folder_uid.into_inner().into())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let metadata = ServerMetadata {
|
||||
uid: ServerId::from_string_lossy(value.uid.inner()),
|
||||
revision: value.revision_ts.into(),
|
||||
metadata_last_updated_ts: value.metadata_last_updated_ts,
|
||||
trashed_ts: value.trashed_ts,
|
||||
folder_id,
|
||||
is_welcome_object: value.is_welcome_object,
|
||||
creator_uid: value.creator_uid.map(|uid| uid.into_inner()),
|
||||
last_editor_uid: value.last_editor_uid.map(|uid| uid.into_inner()),
|
||||
current_editor_uid: value.current_editor_uid.map(|uid| uid.into_inner()),
|
||||
};
|
||||
Ok(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object_permissions::ObjectPermissions> for ServerPermissions {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
value: galaxy_graphql::object_permissions::ObjectPermissions,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let server_object_guests: Result<Vec<ServerObjectGuest>, _> = value
|
||||
.guests
|
||||
.into_iter()
|
||||
.map(|guest| guest.try_into())
|
||||
.collect();
|
||||
let object_permissions = ServerPermissions {
|
||||
space: value.space.try_into()?,
|
||||
guests: server_object_guests?,
|
||||
anyone_link_sharing: match value.anyone_link_sharing {
|
||||
Some(sharing) => Some(sharing.try_into()?),
|
||||
None => None,
|
||||
},
|
||||
permissions_last_updated_ts: value.last_updated_ts,
|
||||
};
|
||||
Ok(object_permissions)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object_permissions::ObjectGuest> for ServerObjectGuest {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: galaxy_graphql::object_permissions::ObjectGuest) -> Result<Self, Self::Error> {
|
||||
let object_guest = ServerObjectGuest {
|
||||
subject: value.subject.try_into()?,
|
||||
access_level: value.access_level,
|
||||
source: match value.source {
|
||||
Some(container) => Some(container.try_into()?),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
Ok(object_guest)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object_permissions::GuestSubject> for ServerGuestSubject {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(
|
||||
value: galaxy_graphql::object_permissions::GuestSubject,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
galaxy_graphql::object_permissions::GuestSubject::UserGuest(user_guest) => {
|
||||
let guest_subject = ServerGuestSubject::User {
|
||||
firebase_uid: user_guest.firebase_uid.into_inner(),
|
||||
};
|
||||
Ok(guest_subject)
|
||||
}
|
||||
galaxy_graphql::object_permissions::GuestSubject::PendingUserGuest(guest) => {
|
||||
Ok(ServerGuestSubject::PendingUser { email: guest.email })
|
||||
}
|
||||
galaxy_graphql::object_permissions::GuestSubject::TeamGuest(team_guest) => {
|
||||
Ok(ServerGuestSubject::Team {
|
||||
team_uid: ServerId::from_string_lossy(team_guest.uid.inner()),
|
||||
})
|
||||
}
|
||||
galaxy_graphql::object_permissions::GuestSubject::Unknown => {
|
||||
anyhow::bail!("Unknown GuestSubject type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object_permissions::LinkSharing> for ServerLinkSharing {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: galaxy_graphql::object_permissions::LinkSharing) -> Result<Self, Self::Error> {
|
||||
Ok(ServerLinkSharing {
|
||||
access_level: value.access_level,
|
||||
source: value.source.map(TryInto::try_into).transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object::Container> for ServerObjectContainer {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: galaxy_graphql::object::Container) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
galaxy_graphql::object::Container::FolderContainer(folder) => {
|
||||
Ok(ServerObjectContainer::Folder {
|
||||
folder_uid: ServerId::from_string_lossy(folder.folder_uid.inner()),
|
||||
})
|
||||
}
|
||||
galaxy_graphql::object::Container::Space(space) => Ok(ServerObjectContainer::Drive {
|
||||
owner: space.try_into()?,
|
||||
}),
|
||||
galaxy_graphql::object::Container::Unknown => {
|
||||
anyhow::bail!("Unknown Container type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<galaxy_graphql::object::Space> for Owner {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: galaxy_graphql::object::Space) -> Result<Self, Self::Error> {
|
||||
let owner = match value.type_ {
|
||||
galaxy_graphql::object::SpaceType::Team => Owner::Team {
|
||||
team_uid: ServerId::from_string_lossy(value.uid.inner()),
|
||||
},
|
||||
galaxy_graphql::object::SpaceType::User => Owner::User {
|
||||
user_uid: UserUid::new(value.uid.inner()),
|
||||
},
|
||||
};
|
||||
Ok(owner)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Owner> for galaxy_graphql::object_permissions::Owner {
|
||||
fn from(owner: Owner) -> Self {
|
||||
use galaxy_graphql::object_permissions::Owner as GraphQLOwner;
|
||||
use galaxy_graphql::object_permissions::OwnerType;
|
||||
match owner {
|
||||
Owner::User { user_uid } => GraphQLOwner {
|
||||
type_: OwnerType::User,
|
||||
uid: Some(cynic::Id::new(user_uid.to_string())),
|
||||
},
|
||||
Owner::Team { team_uid, .. } => GraphQLOwner {
|
||||
type_: OwnerType::Team,
|
||||
uid: Some(cynic::Id::new(team_uid)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user